@sedatdemirAra

Lockfile zehirlenmesi ve npm provenance rehberi

Bir pull request geldi, sadece package-lock.json değişmiş. Diff binlerce satır. Reviewer "lockfile işte, otomatik güncellenir" deyip onaylıyor. Tam da bu an, supply-chain saldırısının en kolay giriş noktası açılıyor.

Lockfile neden hedef oluyor

Lockfile dosyaları (package-lock.json, pnpm-lock.yaml, bun.lockb) bağımlılık ağacının tam bir fotoğrafını tutar: paket adı, sürüm, indirme URL'i, integrity hash. npm install ya da pnpm install komutu çalıştığında registry'ye değil önce bu dosyaya bakar. Saldırgan lockfile içindeki bir paketin resolved URL'ini kendi kontrolündeki bir registry'ye ya da integrity hash'ini farklı bir tarball'a çevirirse, CI/CD pipeline'ı o paketi hiç sorgulamadan indirir.

Bu saldırı tipi ilk kez 2022'de Snyk araştırmacıları tarafından "lockfile-lint" aracıyla birlikte belgelendi. Teknik basit: lockfile'da resolved alanını https://registry.npmjs.org/ yerine https://evil-registry.example.com/ yaparsanız, npm ci bunu kabul eder. Herhangi bir uyarı vermez.

Saldırı senaryosu adım adım

Saldırgan bir open-source projeye pull request açıyor. PR'da package.json'a yeni bir bağımlılık ekleniyor, meşru görünüyor. Ama lockfile diff'inin derinliklerinde, mevcut bir paketin resolved URL'i değiştirilmiş. Reviewer package.json diff'ine bakıyor, makul bulup merge ediyor. CI pipeline'ı npm ci çalıştırıyor ve saldırganın sunucusundan zararlı tarball iniyor.

İkinci bir varyant daha sinsi: package.json hiç değişmiyor. Saldırgan sadece lockfile'daki bir transitive dependency'nin integrity hash'ini değiştiriyor. Bu durumda npm ci hash uyuşmazlığında hata verir, ama npm install vermez; lockfile'ı "düzeltir" ve devam eder. Eğer CI'da npm install kullanılıyorsa (ki birçok projede hâlâ öyle), saldırı sessizce geçer.

Lockfile denetimi: ilk savunma hattı

lockfile-lint aracı, lockfile'daki resolved URL'lerin izin verilen registry listesiyle eşleşip eşleşmediğini kontrol eder.

npx lockfile-lint \
  --path package-lock.json \
  --type npm \
  --allowed-hosts npm \
  --validate-https \
  --validate-integrity

Bu komut üç şeyi doğrular: tüm URL'lerin registry.npmjs.org'a ait olduğunu, HTTPS kullandığını ve integrity hash'inin mevcut olduğunu. CI pipeline'ına eklemek beş dakikalık iş.

pnpm kullanıyorsanız, pnpm v8'den itibaren lockfile'daki resolution alanları otomatik olarak doğrulanır. Ancak pnpm install --frozen-lockfile kullanmak şart; bu flag olmadan pnpm de lockfile'ı sessizce güncelleyebilir.

Bun tarafında bun.lockb ikili (binary) formatta tutuluyor. Bu, metin tabanlı diff manipülasyonunu zorlaştırır ama imkansız kılmaz. bun install --frozen-lockfile flag'i Bun v1.0.15'ten itibaren destekleniyor.

# GitHub Actions CI adımı
- name: Install dependencies
  run: pnpm install --frozen-lockfile

- name: Lint lockfile
  run: npx lockfile-lint --path pnpm-lock.yaml --type yarn --allowed-hosts npm --validate-https

Buradaki --frozen-lockfile kritik. Bu flag, lockfile ile package.json arasında uyuşmazlık varsa kurulumu tamamen durdurur. CI ortamında npm install ya da pnpm install (flag'siz) kullanmak güvenlik açığıdır.

npm provenance: paketin nereden geldiğini kanıtlamak

npm, v9.5.0'dan itibaren provenance (kaynak kanıtı) desteği sunuyor. Bir paket --provenance flag'iyle yayınlandığında, npm CLI Sigstore altyapısını kullanarak bir SLSA provenance attestation üretiyor. Bu attestation, paketin hangi Git commit'inden, hangi CI/CD sisteminde, hangi build komutuyla üretildiğini kriptografik olarak imzalar.

npm publish --provenance

Bu komut çalıştığında arka planda şunlar oluyor:

  1. npm CLI, CI ortamının OIDC token'ını alır (GitHub Actions, GitLab CI destekleniyor).
  2. Bu token'ı Sigstore'un Fulcio sertifika otoritesine gönderir ve kısa ömürlü bir imzalama sertifikası alır.
  3. SLSA v0.2 formatında bir provenance belgesi oluşturur.
  4. Belgeyi Sigstore'un Rekor transparency log'una kaydeder.
  5. Attestation'ı npm registry'ye paket metadata'sıyla birlikte yükler.

Sonuç olarak npmjs.com paket sayfasında yeşil bir "Provenance" rozeti görünür. Herhangi biri npm audit signatures komutuyla bu imzaları doğrulayabilir:

npm audit signatures

Bu komut, yüklü paketlerin registry imzalarını ve varsa provenance attestation'larını kontrol eder. npm v9.5.0+ gerektirir.

GitHub Actions'ta provenance yayını

Provenance yalnızca CI ortamında çalışır; yerel makineden --provenance ile publish yapamazsınız (OIDC token üretilemez). GitHub Actions için minimum konfigürasyon:

name: Publish
on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # Sigstore OIDC için gerekli
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm publish --provenance
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

id-token: write permission'ı olmadan Fulcio sertifika alamazsınız ve build hata verir. Bu satırı unutmak en yaygın hata.

Sigstore'un çalışma modeli

Sigstore üç bileşenden oluşuyor: Fulcio (sertifika otoritesi), Rekor (transparency log), Cosign (imzalama aracı). npm provenance'ı Fulcio ve Rekor'u kullanır.

Geleneksel PGP imzalamadan farkı, anahtar yönetimi gerektirmemesi. Fulcio, CI ortamının OIDC identity'sine dayanarak 10 dakikalık kısa ömürlü sertifikalar üretir. Bu sertifika ile imzalanan provenance belgesi Rekor'a kaydedilir. Rekor append-only bir log olduğu için imza sonradan silinemez ya da değiştirilemez.

Bu model, "paketin GitHub Actions'ta, şu repo'nun şu commit'inden build edildiğini" kriptografik olarak kanıtlar. Bir saldırgan npm token'ınızı çalsa ve kendi makinesinden publish etse, provenance attestation'ı olmaz ya da farklı bir source repository gösterir.

Tüketici tarafında doğrulama

Paket yayınlamıyorsanız bile, kullandığınız paketlerin provenance bilgisini kontrol edebilirsiniz:

# Tüm yüklü paketlerin imzalarını kontrol et
npm audit signatures

# Belirli bir paketin provenance bilgisine bak
npm view express --json | jq '.dist.attestations'

Şu an npm registry'deki paketlerin küçük bir yüzdesi provenance ile yayınlanıyor. Ama büyük paketler hızla adapte oluyor: express, fastify, zod gibi paketler artık provenance ile geliyor.

CI pipeline'ına npm audit signatures adımı eklemek, supply-chain güvenliğini güçlendiren düşük maliyetli bir hamle:

- name: Verify package signatures
  run: npm audit signatures

pnpm ve Bun'da durum

pnpm, lockfile güvenliği konusunda npm'den bir adım önde. pnpm-lock.yaml formatı, her paketin resolution bilgisini ve integrity hash'ini tutar. pnpm install --frozen-lockfile dışında, pnpm v9'dan itibaren pnpm audit komutu bilinen güvenlik açıklarını tarar.

Ancak pnpm henüz Sigstore tabanlı provenance doğrulaması yapmıyor. pnpm, npm registry'den paket çektiği için registry seviyesindeki attestation'lar mevcut ama pnpm CLI'da audit signatures eşdeğeri bir komut yok (pnpm v9.15 itibarıyla).

Bun'da durum benzer. bun.lockb binary formatı lockfile manipülasyonunu zorlaştırıyor ama provenance doğrulaması için Bun CLI'da yerleşik bir araç bulunmuyor. Bun ile çalışıyorsanız, provenance kontrolü için npm CLI'ı ayrıca kullanmak gerekiyor.

Pratik bir güvenlik katmanı: lockfile review policy

Araçlar bir yere kadar korur. Asıl mesele, lockfile değişikliklerinin code review sürecinde nasıl ele alındığı. Birkaç somut öneri:

GitHub'da CODEOWNERS dosyasıyla lockfile'ları güvenlik ekibine atayın:

# .github/CODEOWNERS
package-lock.json @your-org/security-team
pnpm-lock.yaml @your-org/security-team
bun.lockb @your-org/security-team

Bu sayede lockfile değişen her PR, otomatik olarak güvenlik ekibinin onayını bekler.

İkinci önlem: CI'da lockfile diff'ini otomatik analiz ettirin. lockfile-lint temel kontrolleri yapar, ama resolved URL'lerin yanı sıra yeni eklenen paketlerin npm'deki yaşını, indirme sayısını ve maintainer sayısını kontrol eden araçlar da var. Socket.dev'in GitHub uygulaması bunu PR düzeyinde yapıyor.

Ne zaman ne kullanmalı

Paket yayınlıyorsanız, npm publish --provenance kullanın. Maliyet sıfır, CI workflow'una iki satır eklemek yeterli. Tüketici taraftaysanız, CI'da npm ci (asla npm install değil) ve npm audit signatures çalıştırın. Lockfile review'lerini otomatik araçlara bırakmayın, ama lockfile-lint ve CODEOWNERS ile insan review'ünü destekleyin.

CSP ve Trusted Types yazısında tarayıcı taraflı güvenlik katmanlarını ele almıştım. Supply-chain güvenliği, o zincirin build-time tarafı. İkisi birlikte düşünülmezse, birini sağlamlaştırıp diğerini açık bırakmak kolay. Authentication katmanında da benzer bir dikkat gerekiyor; Ed25519 JWT ile servisler arası kimlik doğrulama yazısında kriptografik imzalamanın servis iletişimindeki rolünü anlattım. Sigstore'un yaptığı şey, aynı mantığı paket dağıtım zincirine taşımak.