Mert Özen Her Satırda Daha İleri
Güvenlik Frontend Güvenliği — #08

npm Supply Chain'i Sertleştirmek: Lifecycle Script'leri, Release Cooldown, Provenance ve Trusted Publishing

Mert Özen 8 Kas 2026 10 dk 3 görüntülenme
npm Supply Chain'i Sertleştirmek: Lifecycle Script'leri, Release Cooldown, Provenance ve Trusted Publishing

npm supply chain'i sertleştirmenin üç ayağı var: kurulum script'lerini onay listesiyle sınırlamak, yeni sürümleri birkaç gün bekletmek ve publish token'larını trusted publishing ile ortadan kaldırmak. Bu yazıda npm 11 ve 12'nin allowScripts policy'sini, pnpm'in allowBuilds ve minimumReleaseAge ayarlarını, npm min-release-age'i, npm audit signatures çıktısını ve OIDC tabanlı trusted publishing'i yerel bir registry'de gerçek kurulumlarla deniyoruz.

Sürüm notu: Bu yazıdaki örnekler Angular 22.2, Node.js 24, npm 11.19 ve 12.2, pnpm 12.18 ve yerel registry olarak Verdaccio 7 ile denenmiştir.

Önce Tahmin Et

Bookstore'un Angular projesinde bir geliştirici sabah npm install çalıştırıyor. Lock dosyası yerinde, npm audit temiz, kimse yeni paket eklemedi; sadece bir bağımlılığın patch sürümü range içinde yükseldi. Bu komut, senin makinende senin kullanıcınla kod çalıştırabilir mi? Çalıştırabiliyorsa hangi dosyaları okuyabilir: ~/.npmrc'deki token'ı, CI (Continuous Integration) ortamındaki environment variable'ları, SSH (Secure Shell) anahtarlarını?

Cevap evet. npm, bir paketin preinstall, install ve postinstall script'lerini kurulumun parçası olarak çalıştırır ve bu varsayılan davranış npm 11'de hâlâ açık. npm audit, npm ci, lock dosyası, SRI (Subresource Integrity) ve SBOM (Software Bill of Materials) konularını OWASP (Open Worldwide Application Security Project) serisinin "OWASP A03: Software Supply Chain Failures — NuGetAudit, npm audit ve SRI" yazısında işledik; burada onları tekrar etmiyoruz. Bu yazı bir adım öteye bakıyor: bilinen açığı olmayan ama dün yayımlanmış kötü niyetli bir sürüme karşı ne yapabilirsin?

2025'ten Kalan Dersler

Eylül 2025'te iki olay npm ekosisteminin varsayımlarını sarstı. 8 Eylül'de bir maintainer'ın hesabı sahte bir destek e-postasıyla ele geçirildi ve chalk, debug, ansi-styles gibi 18 popüler pakete zararlı kod eklenmiş sürümler yayımlandı. AWS'in (Amazon Web Services) olay değerlendirmesine göre bu sürümler birkaç saat erişilebilir kaldı, sonra maintainer ve npm ekibi tarafından kaldırıldı. Birkaç gün sonra GitHub'a Shai-Hulud adlı kendini yayan bir worm bildirildi. Zararlı kod postinstall script'inde çalışıyor, bulduğu npm token'larıyla aynı maintainer'ın diğer paketlerine kendini ekleyip yeni sürüm yayımlıyordu. GitHub'ın 22 Eylül 2025 tarihli açıklamasına göre registry'den 500'den fazla paket kaldırıldı.

Saldırı tekniğinin ayrıntısı bizim için önemli değil; çıkardığımız üç ders önemli. Birincisi, kurulum kod çalıştırmaktır: script'i olan bir bağımlılık, npm install anında senin yetkilerinle çalışır. İkincisi, en tehlikeli sürüm en yeni sürümdür: iki olayda da zararlı sürümler saatler içinde fark edildi, yani bekleyen kazandı. Üçüncüsü, uzun ömürlü token'lar yakıttır: worm'un yayılmasını sağlayan şey geliştirici makinelerinde ve CI'da duran publish token'larıydı. Aşağıdaki her ayar bu üç dersten birine karşılık geliyor.

Kurulum Script'lerini Kontrol Altına Almak

Önce Bookstore projesinde hangi bağımlılıkların kurulumda kod çalıştırdığına bakalım. npm 11.19'daki install-scripts komutu bunu doğrudan listeliyor:

npm install-scripts ls
4 packages have install scripts not yet covered by allowScripts:
  esbuild@0.28.2 (postinstall: node install.js)
  lmdb@3.5.6 (install: node-gyp-build-optional-packages)
  msgpackr-extract@3.0.7 (install: node-gyp-build-optional-packages)
  @parcel/watcher@2.6.0 (install: node scripts/build-from-source.js)

Run `npm install-scripts approve <pkg>` to allow, or `npm install-scripts deny <pkg>` to deny.

Dört paket, hepsi Angular CLI (Command-Line Interface) build zincirinden geliyor ve hepsi native binary'yi doğrulamak ya da derlemek için script çalıştırıyor. Davranışı görmek için kendi yerel registry'mde (Verdaccio, 127.0.0.1:19470) book-cover-tools adlı küçük bir paket yayımladım. Script'i yalnızca proje köküne bir işaret dosyası yazıyor:

{
  "name": "book-cover-tools",
  "version": "1.0.1",
  "main": "index.js",
  "scripts": {
    "postinstall": "node install.js"
  }
}
// Native build adımını taklit eder; yalnızca proje köküne bir işaret dosyası yazar.
const fs = require('node:fs');
const path = require('node:path');
const root = process.env.INIT_CWD ?? process.cwd();
fs.writeFileSync(path.join(root, 'postinstall-ran.txt'),
  `book-cover-tools postinstall ran in ${path.relative(root, process.cwd())}\n`);
console.log('book-cover-tools: postinstall executed');

npm 11: varsayılan açık, ignore-scripts ve allowScripts

npm install book-cover-tools
cat postinstall-ran.txt
added 1 package, and audited 2 packages in 393ms

found 0 vulnerabilities
npm warn install-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn install-scripts   book-cover-tools@1.0.1 (postinstall: node install.js)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
book-cover-tools postinstall ran in node_modules/book-cover-tools

npm 11.19 uyarıyor ama script'i yine de çalıştırıyor; işaret dosyası oluştu. En kaba fren --ignore-scripts (ya da .npmrc'de ignore-scripts=true): aynı komut bu bayrakla çalışınca postinstall-ran.txt oluşmadı. Bunun Angular build'ini bozup bozmadığını denedim; ignore-scripts=true ile npm ci ve ardından ng build sorunsuz tamamlandı, çünkü esbuild ve diğerleri platforma özel binary'yi optional dependency olarak zaten indiriyor. Daha ince ayar allowScripts policy'si. --strict-allow-scripts ile uyarı hataya dönüşür:

npm install book-cover-tools --strict-allow-scripts
npm error code ESTRICTALLOWSCRIPTS
npm error --strict-allow-scripts: 1 package(s) have install scripts not covered by allowScripts:
npm error   book-cover-tools@1.0.1 (postinstall: node install.js)
npm error Approve them with `npm install-scripts approve`, deny them with `npm install-scripts deny`, or bypass this check with `--dangerously-allow-all-scripts`.

npm 12: varsayılan kapalı

8 Temmuz 2026'da çıkan npm 12'nin breaking change listesinde şu madde var: bağımlılıkların lifecycle script'leri, kök paketin allowScripts policy'si izin vermedikçe engellenir. Aynı kurulumu npm 12.2.2 ile tekrarladım, sonra paketi onayladım:

npm install book-cover-tools
npm install-scripts approve book-cover-tools
npm rebuild
added 1 package, and audited 2 packages in 496ms

found 0 vulnerabilities
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts   book-cover-tools@1.0.1 (postinstall: node install.js)
...
Approved book-cover-tools:
  added book-cover-tools@1.0.1
rebuilt dependencies successfully

Onay package.json'a sürümüyle birlikte yazılıyor: "allowScripts": {"book-cover-tools@1.0.1": true}. Bu sabitleme (allow-scripts-pin, varsayılan true) asıl savunma: paketin 1.0.2 sürümünü yayımlayıp npm update çalıştırdığımda npm 12 yeni sürümün script'ini yine engelledi ve "1 package had install scripts blocked" uyarısını verdi. Ele geçirilmiş bir maintainer hesabından gelen yeni sürüm, senin dünkü onayınla çalışamıyor. Bookstore'da Angular'ın dört paketini npm install-scripts deny --all ile reddettim ve build hâlâ çalıştı; macOS'ta temiz bir npm ci sonrası listeye bir de fsevents eklendi, onu da reddettim.

pnpm: allowBuilds ve strictDepBuilds

pnpm bu yola daha önce girdi. pnpm 11 (28 Nisan 2026) onlyBuiltDependencies, neverBuiltDependencies, ignoredBuiltDependencies ve ignoreDepScripts ayarlarını kaldırıp tek bir allowBuilds map'ine taşıdı ve strictDepBuilds'i varsayılan true yaptı. pnpm 12.18 ile ayarsız bir projede:

pnpm add book-cover-tools
dependencies:
+ book-cover-tools 1.0.2

Error: ERR_PNPM_IGNORED_BUILDS

  × adding a new package
  ╰─▶ Ignored build scripts: book-cover-tools@1.0.2
  help: Run "pnpm approve-builds" to pick which dependencies should be allowed
        to run scripts.

Kurulum exit code 1 ile bitti; script çalışmadı. İzin pnpm-workspace.yaml'a yazılıyor ve allowBuilds'a eklediğimde script çalıştı:

allowBuilds:
  book-cover-tools: true
.../node_modules/book-cover-tools postinstall$ node install.js
.../node_modules/book-cover-tools postinstall: book-cover-tools: postinstall executed
.../node_modules/book-cover-tools postinstall: Done

Release Cooldown: Yeni Sürümü Bekletmek

İkinci ders: zararlı sürümler saatler içinde yakalanıyor. O zaman yeni yayımlanmış sürümü birkaç gün almamak ucuz bir sigorta. npm bunu 11.10.0 ile (11 Şubat 2026) min-release-age ayarı olarak ekledi; birimi gün. Bugün 8 Kasım 2026; @angular/core 22.2.5, 6 Kasım'da, 22.2.4 ise 29 Ekim'de yayımlandı:

# .npmrc
min-release-age=3
npm install "@angular/core@^22.2.0"
npm ls @angular/core
`-- @angular/core@22.2.4

Range ^22.2.0 olduğu hâlde npm üç günlük olmayan 22.2.5'i atladı ve 22.2.4'ü seçti. Kesin sürüm istersen ne olduğunu da gör:

npm install @angular/core@22.2.5
npm error code ETARGET
npm error notarget No matching version found for @angular/core@22.2.5 with a date before 11/5/2026, 8:34:39 AM.

pnpm'de karşılığı minimumReleaseAge ve birimi dakika. pnpm 11 bunu varsayılan olarak 1440 dakika (1 gün) yaptı, ama bir ayrıntıyı bilmen gerekiyor: varsayılan değer non-strict. Yerel registry'deki paketimin bir günlük sürümü olmadığı için pnpm 12 kurulumu durdurmadı, uyarı verip istisnayı kendisi ekledi:

Added 1 entry to minimumReleaseAgeExclude in pnpm-workspace.yaml (set minimumReleaseAgeStrict to true to gate these updates with a prompt):
  book-cover-tools@1.0.2

Değeri kendin yazarsan minimumReleaseAgeStrict otomatik true olur ve pnpm kurulumu reddeder:

minimumReleaseAge: 4320
Error: ERR_PNPM_NO_MATURE_MATCHING_VERSION

  × adding a new package
  ╰─▶ 1 version does not meet the minimumReleaseAge constraint:
        book-cover-tools@1.0.2 was published at 2026-11-08T05:25:09.529Z,
      within the minimumReleaseAge cutoff (2026-11-05T05:25:36.450Z)

Aynı ayarla @angular/core@^22.2.0 eklediğimde pnpm de 22.2.4'ü seçti.

Renovate ve Dependabot

Güncellemeleri bir bot açıyorsa bekleme süresini orada da tanımla, yoksa bot her gün yeni bir PR (pull request) açıp CI'da kırmızıya düşer. Dependabot dokümanına göre version update'lerde varsayılan bekleme zaten 3 gün; cooldown ile ayarlanabiliyor ve security update'lere uygulanmıyor:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown:
      default-days: 3
      semver-major-days: 14
      exclude:
        - "@bookstore/*"

Renovate'te ayarın adı minimumReleaseAge; dokümanı npm paketleri için 72 saatlik unpublish penceresini gerekçe gösterip 3 days öneriyor ve security update'lerin bu kontrolü atladığını yazıyor:

{
  "extends": ["config:recommended"],
  "packageRules": [
    {
      "matchDatasources": ["npm"],
      "minimumReleaseAge": "3 days"
    }
  ]
}

Bu iki dosyayı dokümana göre yazdım; bir repository'de bot çalıştırmadım.

Provenance ve İmza Doğrulama

npm audit signatures
audited 292 packages in 3s

292 packages have verified registry signatures

140 packages have verified attestations
(use --json --include-attestations to view attestation details)

İki ayrı şey doğrulanıyor. Registry signature, tarball'ın npm registry'sinden değişmeden geldiğini gösterir. Attestation (provenance) ise paketin hangi repository'den, hangi CI workflow'undan build edildiğini SLSA (Supply-chain Levels for Software Artifacts) formatında belgeler; vite için JSON (JavaScript Object Notation) çıktısında "predicateType": "https://slsa.dev/provenance/v1" görünüyor. Dikkat çekici olan, 292 paketin yalnız 140'ında attestation olması: @angular/*, rxjs, typescript ve tslib registry imzalı ama provenance'sız yayımlanıyor. Bu, paketlerin güvensiz olduğu anlamına gelmez; provenance'ın henüz evrensel olmadığını gösterir. pnpm'in trustPolicy: no-downgrade ayarı tam bu boşluğa bakar: daha önce provenance'la yayımlanmış bir paketin yeni sürümü provenance'sız gelirse kurulumu durdurur. Ele geçirilmiş bir hesaptan elle yapılan publish genelde tam olarak böyle görünür.

Kendi Paketin: Trusted Publishing ve Token'lar

Üçüncü ders token'larla ilgili ve burada npm tarafı çok değişti. GitHub 5 Kasım 2025'te classic token oluşturmayı kapattı, 9 Aralık 2025'te mevcut tüm classic token'ları iptal etti. Artık npm login iki saatlik session token veriyor ve yazma yetkili granular token'ların ömrü en fazla 90 gün. Bookstore ekibi iç paketlerini npm'e publish ediyorsa doğru yol trusted publishing: CI, OIDC (OpenID Connect) ile kısa ömürlü bir kimlik alır, ortada saklanacak bir token kalmaz. OIDC mekaniğini "GitHub Actions ile Deploy 1: OIDC ile Azure App Service ve Şifresiz Cloud Erişimi" yazısında görmüştük. npm dokümanına göre en az npm CLI 11.5.1 ve Node 22.14.0 gerekiyor:

# .github/workflows/publish.yml
name: Publish Package
on:
  push:
    tags:
      - 'v*'
permissions:
  id-token: write
  contents: read
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: '24'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm test
      - run: npm publish

Paket tarafında ilişkiyi npmjs.com'daki paket ayarlarından ya da npm 11.10 ile gelen npm trust github <paket> --file publish.yml --repository <owner/repo> --allow-publish komutuyla kurarsın. Public repository ve public paket için provenance otomatik üretilir, --provenance bayrağına gerek kalmaz. Son adımı atlama: paketin "Publishing access" ayarında "Require two-factor authentication and disallow tokens" seçeneğini işaretle, yoksa eski bir token hâlâ publish edebilir. Bu workflow'u gerçek bir paketle çalıştırmadım; dosya npm dokümanındaki örneğe göre.

Üretimde Dikkat

Cooldown'un bilinçli istisnası güvenlik yaması. Kritik bir advisory 22.2.5'te kapatıldıysa üç gün beklemek doğru değil. Yamayı açıkça ve izlenebilir biçimde al:

npm install @angular/core@22.2.5 --min-release-age-exclude=@angular/core

Bu komut 22.2.5'i kurdu. min-release-age-exclude npm 11.17 ile geldi; pnpm'de karşılığı minimumReleaseAgeExclude. İstisnayı PR açıklamasına advisory numarasıyla yaz ve iş bitince kaldır. Kalıcı istisna listesi zamanla cooldown'u anlamsızlaştırır.

Policy'leri CI'da da uygula, yoksa sadece geliştirici makinesini korumuş olursun. Script onay listesi platforma göre değişebilir; macOS'ta fsevents göründü, Linux runner'da görünmeyecek. Onayları package.json'da tut, code review'dan geçsin; dangerously-allow-all-scripts ya da dangerouslyAllowAllBuilds bir pipeline dosyasında görürsen nedenini sor. Action'ları commit SHA'sıyla (Secure Hash Algorithm) sabitlemek, kurulum adımlarını ve token'ları pipeline genelinde sıkılaştırmak "GitHub Actions Güvenliği: SHA Pinning, Lockfile, pull_request_target ve Script Injection'dan Korunmak" yazısının ve ilerideki DevSecOps serisinin konusu.

Sık Düşülen Tuzaklar

Birincisi, ignore-scripts'in her şeyi çözdüğünü sanmak. Kurulumda çalışmayan kod, uygulama ya da build çalışırken yine çalışır; script kontrolü saldırı yüzeyini küçültür, paket içeriğini denetlemez. Bu yüzden cooldown ve provenance ayrı katmanlar olarak gerekli.

İkincisi, pnpm'in varsayılan bir günlük bekleme süresine güvenmek. Gördüğümüz gibi varsayılan non-strict ve istisnayı sessizce dosyaya yazabiliyor. Değeri pnpm-workspace.yaml'a açıkça yaz ki strict mod devreye girsin. pnpm 12 ayrıca yanlış yazılmış ayar adlarını artık raporluyor; eski sürümlerde minimumReleaseAge'deki bir yazım hatası policy'yi hiçbir uyarı olmadan devre dışı bırakıyordu.

Üçüncüsü, script onayını sürümsüz yapmak. allow-scripts-pin=false ile onay "her sürüm" anlamına gelir ve bir sonraki zararlı sürüm de onaylı sayılır.

Dördüncüsü, provenance yokluğunu alarm, varlığını garanti saymak. Provenance build'in nereden geldiğini kanıtlar; o repository'deki kodun zararsız olduğunu kanıtlamaz.

Küçük Bir Deneme

★☆☆ Kendi Angular projende npm install-scripts ls çalıştır, listedeki paketleri deny ile reddet ve ng build ile ng test'in hâlâ geçtiğini doğrula.

★★☆ .npmrc'ye min-release-age=7 yaz, npm view @angular/core time --json çıktısıyla hangi sürümün seçilmesi gerektiğini tahmin et, sonra npm install ile tahminini doğrula.

★★★ Yerel bir Verdaccio kur, script'li bir paketin iki sürümünü yayımla. npm 12 ile ilkini onayla, ikincisine güncelle ve onayın neden taşınmadığını package.json'daki allowScripts key'ine bakarak açıkla.

Bir sonraki ve serinin son yazısında geriye bakıyoruz: Angular 11'den 22'ye ve tarayıcılarda frontend güvenliğinde neler değişti, eski bir Angular projesini denetlerken hangi sürümde hangi ayarın eksik olduğunu nasıl bulursun.