Express'te Güvenlik: helmet, CORS, Rate Limiting, JWT Doğrulama ve Input Sınırları
Express API'yi güvenli hâle getirmek için helmet ile security header'ları eklenir, CORS bir origin allow-list'iyle sınırlanır, rate limit trust proxy doğru ayarlanarak uygulanır, JWT jose ile sabit algoritma, issuer, audience ve süre kontrolüyle doğrulanır ve her sipariş erişiminde sahiplik kontrol edilir. Gerçek çıktılarla X-Forwarded-For ile aşılan limit, her origin'i yansıtan CORS ayarı, alg none ve algorithm confusion'ın 401'i, BOLA'ya karşı 404 ve prototype pollution.
Sürüm notu: Bu yazıdaki örnekler Node.js 26.13.1 (LTS), Express 5.3.0, helmet 8.3.0, cors 2.8.6, express-rate-limit 8.7.4, jose 6.2.17 ve TypeScript 7.0.2 ile çalıştırılmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) API'ye rate limiting ekliyoruz: aynı IP adresinden dakikada en fazla 3 istek. Express'in trust proxy ayarı ise req.ip'nin nereden okunacağını belirliyor. Aşağıdaki lab, aynı makineden altı istek gönderiyor; her istek X-Forwarded-For header'ında farklı bir client IP'si olduğunu iddia ediyor. API'nin önünde hiçbir proxy yok.
// Her biri X-Forwarded-For'da farklı bir client IP'si iddia eden altı istek; limit pencere başına 3
import express from "express";
import { rateLimit } from "express-rate-limit";
for (const setting of [0, 1, true]) {
const app = express();
app.set("trust proxy", setting);
app.use(rateLimit({ windowMs: 60_000, limit: 3, standardHeaders: "draft-8", legacyHeaders: false }));
app.get("/", (req, res) => res.json({ ip: req.ip }));
const server = app.listen(0, "127.0.0.1");
await new Promise((resolve) => server.once("listening", resolve));
const { port } = server.address() as { port: number };
const statuses: number[] = [];
for (let i = 1; i <= 6; i++) {
const res = await fetch(`http://127.0.0.1:${port}/`, { headers: { "x-forwarded-for": `203.0.113.${i}` } });
statuses.push(res.status);
}
console.log(`trust proxy = ${String(setting).padEnd(4)} -> ${statuses.join(" ")}`);
server.close();
}
Üç ayarın hangisinde dördüncü istekten itibaren 429 gelir?
node labs/15/trust-proxy.ts
trust proxy = 0 -> 200 200 200 429 429 429
trust proxy = 1 -> 200 200 200 200 200 200
ValidationError: The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting. See https://express-rate-limit.github.io/ERR_ERL_PERMISSIVE_TRUST_PROXY/ for more information.
trust proxy = true -> 200 200 200 200 200 200
Yalnız 0'da limit çalıştı. 1 "önümde bir proxy var, X-Forwarded-For'un son değerine güven" demek; proxy olmadığı için o değeri client'ın kendisi yazdı ve her istek farklı biri gibi göründü. true ise header'daki her şeye güveniyor; express-rate-limit bunu ilk istekte (çıktıdaki stack trace'i kısalttım) ERR_ERL_PERMISSIVE_TRUST_PROXY uyarısıyla yakalıyor ama isteği engellemiyor. Kural: trust proxy, API'nin önündeki gerçek proxy sayısıdır, ne eksik ne fazla. Bu yazıda Bookstore API'sini dışarıya karşı sağlamlaştırıyoruz: security header'ları, CORS (Cross-Origin Resource Sharing; tarayıcının başka bir origin'e yapılan isteklere izin kuralları), rate limiting, JWT (JSON Web Token; imzalı, kimlik bilgisi taşıyan token) doğrulama, endpoint ve object seviyesinde yetkilendirme ve input sınırları. Kavramların kendisi Web Güvenliği (OWASP Top 10) ve Authentication ve Authorization serilerinde; burada Express'teki uygulanışına bakıyoruz.
helmet ve Security Header'ları
13'teki ilk curl -i çıktısında X-Powered-By: Express vardı: saldırgana hangi framework'ün çalıştığını söyleyen, hiçbir işe yaramayan bir header. 11'de kapatacağımızı söylemiştik. createApp'in başı artık şöyle:
const app = express();
// helmet de bu header'ı kaldırır; bu satır, helmet'e hiç ulaşmayan response'larda da kapalı kalmasını sağlar
app.disable("x-powered-by");
// req.ip, X-Forwarded-For'dan yalnız bu kadar proxy adımı için okunur; 0 = socket adresi
app.set("trust proxy", options.trustProxy);
// Global middleware'ler her istekte bu sırayla çalışır
app.use(requestContext);
// same-origin yerine same-site: aynı sitenin başka portundaki Angular istemcisi kapakları gösterebilir
app.use(helmet({ crossOriginResourcePolicy: { policy: "same-site" } }));
helmet tek bir middleware'le bir grup header'ı varsayılan değerleriyle ekliyor:
curl -s -o /dev/null -D - "http://127.0.0.1:3001/api/books?pageSize=1"
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0
Bir JSON API için en önemlileri X-Content-Type-Options: nosniff (tarayıcı JSON'u HTML gibi yorumlamasın) ve HTTPS arkasında Strict-Transport-Security. CSP (Content Security Policy; sayfanın hangi kaynaklardan script ve stil yükleyebileceğini söyleyen header) asıl HTML sayfaları için anlamlı; API'de zararsız. Değiştirdiğimiz tek varsayılan Cross-Origin-Resource-Policy: helmet onu same-origin yapıyor ve bu, 4200 portundaki Angular uygulamasının 08'de yüklediğimiz kapak resimlerini <img> ile göstermesini engeller. localhost:4200 ile localhost:3000 farklı origin ama aynı site; same-site bu yüzden yetiyor. Bunu tarayıcıda denemedim, helmet dokümanına ve header'ın tanımına dayanıyor. ASP.NET Core'da bu header'ları genellikle bir middleware ya da reverse proxy ekler; tek bir standart paket yok.
CORS: Origin Allow-List
Tarayıcı, başka bir origin'den gelen JavaScript'in API'yi okumasına ancak API izin verirse izin veriyor. İzin listesi configuration'dan geliyor (CORS_ORIGINS=http://localhost:4200; zod her değerin bir URL olduğunu doğruluyor):
app.use(
cors({
origin: [...options.corsOrigins],
methods: ["GET", "POST"],
allowedHeaders: ["authorization", "content-type", "x-request-id"],
exposedHeaders: ["location", "x-request-id"],
maxAge: 600,
}),
);
Angular'ın Authorization header'lı bir POST'tan önce gönderdiği preflight isteği, izinli origin ve yabancı origin için:
curl -s -o /dev/null -D - -X OPTIONS -H "origin: http://localhost:4200" -H "access-control-request-method: POST" -H "access-control-request-headers: authorization,content-type" http://127.0.0.1:3001/api/orders
curl -s -o /dev/null -D - -X OPTIONS -H "origin: https://evil.example" -H "access-control-request-method: POST" http://127.0.0.1:3001/api/orders
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:4200
Vary: Origin
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: authorization,content-type,x-request-id
Access-Control-Max-Age: 600
HTTP/1.1 204 No Content
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: authorization,content-type,x-request-id
Access-Control-Max-Age: 600
İkinci cevapta Access-Control-Allow-Origin yok; tarayıcı isteği göndermez. CORS'un bir sunucu güvenlik kontrolü olmadığını unutma: curl ya da bir script bu header'lara hiç bakmaz. CORS yalnız tarayıcıdaki kullanıcıyı başka sitelerin script'lerine karşı korur. exposedHeaders'a location ekledik, yoksa Angular 201 cevabındaki Location header'ını okuyamaz. Klasik tuzak, "çalışsın da" diye yazılan ayar:
// YANLIŞ: origin: true her Origin'i geri yansıtır; credentials: true ile tarayıcı cookie'leri de gönderir
app.use(cors({ origin: true, credentials: true }));
access-control-allow-origin: https://evil.example
access-control-allow-credentials: true
Spec, * ile credentials'ı birlikte kabul etmiyor; origin: true bu kuralı her origin'i tek tek yansıtarak delip geçiyor. API cookie ile çalışsaydı, kullanıcının giriş yaptığı tarayıcıda herhangi bir site onun siparişlerini okuyabilirdi. Bookstore bearer token kullandığı için credentials'a gerek yok.
Rate Limiting ve trust proxy
express-rate-limit bütün /api isteklerini IP başına dakikada RATE_LIMIT_PER_MINUTE (varsayılan 120) ile sınırlıyor; 429'u diğer hatalar gibi Problem Details olarak dönmek için limitin handler'ı hatayı hata middleware'ine veriyor:
app.use(
"/api",
rateLimit({
windowMs: 60_000,
limit: options.rateLimitPerMinute,
standardHeaders: "draft-8",
legacyHeaders: false,
// 429, diğer her hata gibi Problem Details olarak
handler: (req, res, next) => next(new HttpError(429, "too many requests, try again later")),
}),
);
Limiti 5'e indirip altı istek gönderdim:
200 200 200 200 200 429
HTTP/1.1 429 Too Many Requests
RateLimit: "5-in-1min"; r=0; t=60
Retry-After: 60
Content-Type: application/problem+json; charset=utf-8
{"type":"https://tools.ietf.org/html/rfc6585#section-4","title":"Too Many Requests","status":429,"detail":"too many requests, try again later","instance":"/api/books?pageSize=1","traceId":"req-15r"}
RateLimit header'ı IETF'in (internet standartlarını yazan kuruluş) rate limit header taslağının yeni biçimi: kalan istek (r) ve pencerenin sıfırlanmasına kalan saniye (t). Varsayılan store process'in belleğinde; 14'teki gibi dört replika varsa her biri ayrı sayar ve gerçek limit dört katı olur. Birden çok instance'ta Redis gibi paylaşılan bir store kullanılmalı. IP başına limit kaba bir araç: kurumsal bir müşterinin bütün kullanıcıları tek bir NAT (birçok cihazın tek bir public IP arkasından çıkması) IP'sinden gelebilir. Kimlik doğrulanmış endpoint'lerde keyGenerator ile kullanıcıya göre saymak daha adil. ASP.NET Core'daki karşılığı AddRateLimiter; mantık aynı.
trust proxy'yi configuration'da bilerek bir sayı yaptık (TRUST_PROXY, 0–5) ve true'ya izin vermiyoruz. Bir load balancer'ın arkasında 1, doğrudan bağlantıda 0. Önce Tahmin Et'te gördüğümüz gibi yanlış sayı, rate limiter'ı ve log'lardaki IP'yi aynı anda yanıltır.
JWT Doğrulama: AddJwtBearer'ın Yaptıkları
ASP.NET Core'da AddJwtBearer birkaç satırlık bir ayar; arkasında JWKS (JSON Web Key Set; identity provider'ın public key'lerini yayımladığı JSON belgesi) indirmek, imzayı doğrulamak, issuer, audience ve süreyi kontrol etmek var. Node'da bunları jose paketiyle açıkça yazıyoruz:
// ASP.NET Core'da AddJwtBearer'ın senin yerine yaptıkları, açık hâliyle: JWKS'ten key, imza, alg, iss, aud, exp
export function createJwtVerifier({ issuer, audience, jwksUrl }: JwtOptions): TokenVerifier {
// Key'ler ilk kullanımda indirilir, cache'lenir ve token bilinmeyen bir kid taşıyınca yeniden indirilir
const keys = createRemoteJWKSet(new URL(jwksUrl));
return async (token) => {
try {
const { payload } = await jwtVerify(token, keys, {
issuer,
audience,
// Sabitlendi: token kendisi için "none" ya da HS256 seçemez
algorithms: ["RS256"],
requiredClaims: ["sub", "exp"],
// ASP.NET Core varsayılan olarak 5 dakika saat farkına izin verir; jose hiç izin vermez
clockTolerance: 30,
});
return { userId: payload.sub!, roles: rolesOf(payload) };
} catch (err) {
throw new InvalidTokenError((err as { code?: string }).code ?? "ERR_UNKNOWN");
}
};
}
Middleware, Authorization: Bearer ... header'ını okuyor, token'ı doğruluyor ve sonucu res.locals.user'a koyuyor. Reddetme sebebi log'a gidiyor; client yalnız token'ın kabul edilmediğini öğreniyor:
export function authenticate(verify: TokenVerifier): RequestHandler {
return async (req, res, next) => {
const match = BEARER.exec(req.headers.authorization ?? "");
if (!match) {
res.setHeader("www-authenticate", "Bearer");
throw new HttpError(401, "a bearer token is required");
}
try {
res.locals.user = await verify(match[1]);
} catch (err) {
// Sebep log'a gider; client yalnız token'ın kabul edilmediğini öğrenir
logger.info({ reason: err instanceof InvalidTokenError ? err.code : (err as Error).message }, "token rejected");
res.setHeader("www-authenticate", 'Bearer error="invalid_token"');
throw new HttpError(401, "the access token is invalid or expired");
}
next();
};
}
Geliştirmede gerçek bir identity provider'a ihtiyaç duymamak için scripts/dev-issuer.ts yazdık; dotnet user-jwts'in karşılığı. Her açılışta bellekte yeni bir RSA (public/private key çiftiyle çalışan asimetrik algoritma) key çifti üretiyor, public key'i /.well-known/jwks.json'da yayımlıyor ve POST /token ile token veriyor. Key diske yazılmıyor, repo'ya girmiyor; production'da NODE_ENV=production iken açılmayı reddediyor. API, AUTH_ISSUER, AUTH_AUDIENCE ve AUTH_JWKS_URL ile buna bağlanıyor. labs/15/jwt-attacks.ts geçerli ve sahte token'larla bir dizi istek gönderiyor (token'ların kendisini yazıda göstermiyorum):
npm run dev:issuer
node labs/15/jwt-attacks.ts
no token 401
Ada places an order 201
Ada reads her order 200
Alan reads Ada's order 404
Grace (StoreStaff) reads Ada's order 200
Ada calls the export 403
Ada sends customerId: alan in the body 400
expired two minutes ago 401
token for another audience 401
alg "none", no signature 401
HS256 signed with the public key 401
RS256 signed with another key 401
Son beş satırın sebebi API log'unda:
"reason":"ERR_JWT_EXPIRED"
"reason":"ERR_JWT_CLAIM_VALIDATION_FAILED"
"reason":"ERR_JOSE_ALG_NOT_ALLOWED"
"reason":"ERR_JOSE_ALG_NOT_ALLOWED"
"reason":"ERR_JWS_SIGNATURE_VERIFICATION_FAILED"
İki dakika önce süresi dolan token, 30 saniyelik toleransı aştığı için reddedildi; başka bir API için verilmiş token aud kontrolüne takıldı. alg: "none" ve "algorithm confusion" aynı yerde duruyor. Algorithm confusion'da saldırgan token'ı HS256 (HMAC, yani tek bir paylaşılan secret ile imza üreten simetrik algoritma) ile imzalıyor ve secret olarak API'nin RSA public key'ini kullanıyor; public key ise herkesin elinde. İkisinde de koruma aynı: algorithms: ["RS256"], token'ın header'ı ne derse desin başka bir algoritmayı kabul etmiyor. Bu iki saldırı, algoritmayı token'dan okuyan eski kütüphanelerde gerçekten çalıştı; algoritmayı sabitlemek bu yüzden şart. Son satırda saldırgan kendi key'iyle imzalayıp gerçek key'in kid'ini yazdı; imza tutmadı. Client'a giden cevap her durumda aynı:
HTTP/1.1 401 Unauthorized
x-request-id: req-15e
www-authenticate: Bearer error="invalid_token"
Content-Type: application/problem+json; charset=utf-8
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.2","title":"Unauthorized","status":401,"detail":"the access token is invalid or expired","instance":"/api/orders/ORD-1001","traceId":"req-15e"}
ASP.NET Core bu durumda varsayılan olarak error_description ile sebebi de yazar; biz sebebi log'da traceId ile bulunabilir tuttuk. Token'ın nasıl alındığı, refresh token ve OpenID Connect akışları Authentication ve Authorization serisinin konusu.
Authorization: BOLA ve Rol Kontrolü
Lab çıktısının ortası yetkilendirmeyle ilgili. BOLA (Broken Object Level Authorization; kullanıcının ID'sini değiştirerek başkasının kaydına ulaşabilmesi) API'lerde en sık görülen açık. GET /api/orders/:id artık siparişin sahibine bakıyor:
// Object seviyesinde yetkilendirme: müşteri kendi siparişlerini, mağaza personeli hepsini görür
async getForUser(id: string, user: Principal): Promise<Order> {
const order = await this.#orders.findById(id);
// Başkasının siparişi "forbidden" değil "not found": cevap siparişin var olduğunu doğrulamamalı
if (!order || (order.customerId !== user.userId && !user.roles.includes("StoreStaff"))) {
throw new OrderNotFoundError(id);
}
return order;
}
Alan, Ada'nın siparişi için 404 aldı; 403 dönseydik sipariş numaralarını deneyerek hangilerinin var olduğunu öğrenebilirdi. Kontrol service'te, çünkü bu bir iş kuralı ve 16'da HTTP olmadan test edilecek. İkinci değişiklik: sipariş sahibi artık body'den değil token'ın sub claim'inden geliyor ve placeOrderBody şemasından customerId kaldırıldı. Ada body'ye "customerId": "alan" yazınca 400 aldı (strictObject, 12). Bu, mevcut client'lar için bir breaking change; Angular istemcisinin isteğinden alanı silmek gerekiyor. Endpoint seviyesinde ise export yalnız StoreStaff rolüne açık:
// Her sipariş endpoint'i giriş yapmış bir kullanıcı ister; export yalnız mağaza personeli için
// "/export", "/:id"'den önce: "export" hiçbir zaman id sanılmaz
export function ordersRouter(orders: OrderService, authenticated: RequestHandler): Router {
return Router()
.use(authenticated)
.post("/", placeOrder(orders))
.get("/export", requireRole("StoreStaff"), exportOrders(orders))
.get("/:id", getOrder(orders));
}
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.4","title":"Forbidden","status":403,"detail":"this endpoint requires the StoreStaff role","instance":"/api/orders/export","traceId":"req-15f"}
06'daki CPU ağırlıklı fiyat raporu (/api/reports) da StoreStaff'a, /diagnostics ise Admin'e kapandı: kimliği belirsiz birinin event loop'u meşgul eden bir raporu tetikleyebilmesi kolay bir DoS (Denial of Service; servisi meşgul edip kullanılamaz hâle getirme) yoluydu. Katalog (/api/books) herkese açık kalıyor.
Input Sınırları ve Prototype Pollution
Sınırların çoğunu önceki yazılarda koyduk: body en fazla 64 KB (12, aşınca 413 ve bağlantı kapanıyor), sayfa boyutu en fazla 50, sipariş satırı en fazla 50, ISBN (kitapların uluslararası standart numarası) ve metin alanlarında uzunluk sınırı, HTTP server'da headersTimeout ve requestTimeout (10). Node'a özgü bir risk daha var: prototype pollution. JSON.parse, "__proto__" anahtarını sıradan bir property yapar; tehlike, bu object'i elle yazılmış bir "deep merge" fonksiyonuyla başka bir object'e kopyalayınca başlar:
// JSON.parse "__proto__"'yu sıradan bir property yapar; tehlike, başka bir object'e merge edilince başlar
const body = JSON.parse('{"lines":[{"isbn":"9780441172719","quantity":1}],"__proto__":{"isStaff":true}}');
// 12'deki zod şeması bilinmeyen anahtarı, herhangi bir kod onu merge edemeden reddeder
const result = placeOrderBody.safeParse(body);
console.log("zod:", result.success ? "accepted" : result.error.issues.map((i) => i.message).join("; "));
// Elle yazılmış "deep merge" yardımcılarında görülen türden naif bir recursive merge
function merge(target: Record<string, any>, source: Record<string, any>): Record<string, any> {
for (const key of Object.keys(source)) {
if (typeof source[key] === "object" && source[key] !== null) {
target[key] = merge(target[key] ?? {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
merge({}, body);
const user = {};
console.log("after merge, any object is staff:", (user as { isStaff?: boolean }).isStaff);
node labs/15/prototype-pollution.ts
node --disable-proto=delete labs/15/prototype-pollution.ts
zod: Unrecognized key: "__proto__"
after merge, any object is staff: true
zod: Unrecognized key: "__proto__"
after merge, any object is staff: undefined
Merge, target["__proto__"] üzerinden Object.prototype'a yazdı ve process'teki boş bir object bile artık isStaff taşıyor; bir yetki kontrolü user.isStaff'a bakıyorsa herkes personel olur. Savunma üç katmanlı: request'i önce şemadan geçirmek (bizim API'de strictObject bu anahtarı zaten reddediyor), deep merge yazmamak ya da __proto__, constructor ve prototype anahtarlarını atlamak, ve process'i --disable-proto=delete ile çalıştırmak. Son seçenek __proto__ accessor'ını tamamen kaldırıyor; ikinci koşuda kirlilik oluşmadı. Bağımlılıklardan biri __proto__'ya güveniyorsa bozulabilir, bu yüzden testlerle birlikte açılmalı.
OWASP API Top 10 Eşlemesi ve Bağımlılıklar
API Güvenliği (OWASP API Top 10) serisi her maddeyi ayrıntılı anlatıyor. Bu yazıda yaptıklarımızın 2023 listesine karşılığı:
| OWASP API 2023 | Bookstore API'de |
|---|---|
API1 Broken Object Level Authorization | getForUser: başkasının siparişi 404 |
API2 Broken Authentication | jose ile imza, alg, iss, aud, exp kontrolü |
API3 Broken Object Property Level Authorization | strictObject, customerId token'dan |
API4 Unrestricted Resource Consumption | Rate limit, body ve sayfa boyutu sınırı, timeout'lar |
API5 Broken Function Level Authorization | requireRole: export, rapor, diagnostics |
API8 Security Misconfiguration | helmet, CORS allow-list, X-Powered-By kapalı, hata cevabında ayrıntı yok |
Bağımlılık tarafında 04'te anlattığımız npm audit CI'da çalışmalı. Production bağımlılıklarını tarayınca:
npm audit --omit=dev
found 0 vulnerabilities
Sıfır bugünün sonucu: her yeni paket (bu yazıda dört tane ekledik) attack surface'i (saldırganın kullanabileceği giriş noktalarını) büyütür ve yarın yeni bir açık çıkabilir. Supply chain kontrol listesi 04'te.
Üretimde Dikkat
trust proxy'yi gerçek topolojiye göre ayarla ve deploy'dan sonra log'daki IP'lerin gerçekten client IP'si olduğunu kontrol et.
JWKS'e erişilemezse ne olacağını bil. createRemoteJWKSet key'leri cache'liyor; identity provider kısa süre erişilemese de bilinen key'lerle doğrulama sürer, ama yeni bir kid geldiğinde istek başarısız olur.
Rate limit store'unu paylaş. Birden çok replikada bellek içi sayaç, limiti replika sayısıyla çarpar.
CORS listesini ortama göre ver. Geliştirmede localhost:4200, production'da yalnız gerçek alan adı; listeye "geçici" eklenen origin'ler kalıcı olur.
Reddedilen token'ları say. token rejected log satırlarındaki ani artış, saat farkı sorunu ya da birinin token denemesi olabilir.
Sık Düşülen Tuzaklar
trust proxy = true. Herkes istediği IP'yi yazar; rate limit ve IP bazlı kurallar anlamını yitirir.
cors({ origin: true, credentials: true }). Her origin'e kullanıcının kimliğiyle okuma izni.
Algoritmayı sabitlememek ve jwt.decode ile yetinmek. Decode imzayı kontrol etmez; doğrulama her zaman beklenen algoritma, issuer ve audience ile yapılır.
Sahiplik kontrolünü unutmak. Token geçerli diye her sipariş okunabilir olmamalı; kontrol her object erişiminde.
Kimliği body'den almak. customerId, role, isStaff gibi alanlar client'tan gelmez; token'dan ya da veritabanından gelir.
Token'ı log'lamak. Authorization header'ı redaction listesinde (14); hata ayıklarken bile token'ı log'a yazma.
Küçük Bir Deneme
★☆☆ CORS_ORIGINS'e * yazıp API'yi başlat. Hangi satır, hangi mesajla açılışı engelliyor? Neden bu davranış istenen şey?
★★☆ Sipariş endpoint'i için ikinci bir rate limiter ekle: kullanıcı başına (token'daki sub) dakikada 10 sipariş. keyGenerator'ı yaz ve limiti authenticate'ten sonra bağla; sıranın neden önemli olduğunu açıkla.
★★★ dev-issuer'ı yeniden başlat ve API'yi kapatmadan eski bir token ile, sonra yeni bir token ile istek gönder. createRemoteJWKSet yeni kid'i ne zaman indiriyor? Key rotation sırasında eski ve yeni key'in JWKS'te birlikte durması neden gerekiyor?
API artık kimin ne yapabileceğini biliyor, ama bunların hepsini elle denedik. Bir sonraki yazıda aynı senaryoları kalıcı testlere çeviriyoruz: node:test, mocking, coverage ve supertest ile createApp'i port açmadan, sahte repository'lerle test etmek.