05 - API 보안: OAuth 2.1, JWT 및 속도 제한
API는 최신 애플리케이션의 중추입니다. 모든 마이크로서비스, 모든 모바일 앱, 모든 통합 SaaS passes through HTTP endpoints that expose critical data and functionality. 보고서에 따르면 2024년 솔트 보안 API 보안 현황, API 공격이 증가했습니다. 지난 12개월간 167%, 조직의 94%가 적어도 하나의 경험을 갖고 있습니다. 연중 API 관련 보안사고입니다. 이는 통계와 멀지 않습니다. 일상 현실: 이러한 취약점의 대부분은 개발자가 사용하는 패턴과 관련이 있습니다. 그들은 매일 복제됩니다.
문제는 구조적이다. API는 기능을 염두에 두고 설계되었으며 보안이 제공됩니다. 나중에 표면층으로 추가됩니다. JWT 토큰을 추가하고 다음을 고려합니다. 작업을 마쳤습니다. 그러나 OWASP API Security Top 10:2023은 가장 중요한 벡터가 기술적이지 않다는 것을 보여줍니다. 그들은 논리적입니다. 손상된 객체 수준 권한 부여, 손상된 기능 수준 권한 부여, 무제한 자원 소비. 어떤 프레임워크도 자동으로 해결하지 못하는 취약점 고의적인 건축 결정.
이 문서에서는 OWASP API부터 최신 API의 전체 공격 표면을 안내합니다. 범위가 있는 OAuth 2.1의 토큰 버킷 및 슬라이딩 윈도우 알고리즘을 사용한 상위 10:2023 속도 제한 올바른 CORS 구성부터 API 게이트웨이 패턴까지 입력 검증까지 세분화됩니다. 최종 Angular 관련 체크리스트가 포함된 각 섹션의 실용적인 Node.js/Express 코드.
무엇을 배울 것인가
- OWASP API 보안 상위 10:2023: 실제 사례가 포함된 가장 심각한 10가지 취약점
- 속도 제한: Node.js에서 Redis를 사용한 토큰 버킷 및 슬라이딩 윈도우 구현
- 세분화된 범위를 갖춘 OAuth 2.1: OAuth 2.0 및 필수 PKCE와의 차이점
- JWT 모범 사례: 보안 알고리즘, 토큰 순환, 해지 및 블랙리스트 작성
- API 키와 Bearer 토큰: 언제 어떤 접근 방식을 사용해야 하며 어떻게 안전하게 관리해야 할까요?
- TypeScript용 Zod를 사용한 입력 검증 및 정리
- 보안 CORS 구성: 원본 화이트리스트 및 자격 증명 관리
- API 게이트웨이 패턴: 중앙 집중식 인증, 회로 차단기, 보안 로깅
- 모니터링 및 경고: 실시간으로 공격 패턴 탐지
- 프런트엔드의 안전한 API 호출을 위한 각도 체크리스트
OWASP API 보안 상위 10:2023
OWASP API Security Top 10:2023 및 가장 중요한 위험을 이해하기 위한 참조 목록 최신 API에서. 2019년 버전과 비교하면 세 가지 새로운 카테고리가 도입되고 우선순위가 재정렬됩니다. 실제 사고 데이터를 기반으로 합니다. 이론적인 목록이 아닙니다. 각 항목은 취약점에 해당합니다. 최근 몇 년간 실제 침해를 일으킨 사례가 문서화되어 있습니다.
API1:2023 - 손상된 개체 수준 인증(BOLA)
BOLA는 3회 연속 1위를 차지했으며 전체 공격의 약 40%를 차지합니다.
문서화된 API. API는 사용자를 확인하지 않고 ID로 식별된 리소스를 반환합니다.
인증됨에는 해당 특정 리소스에 대한 권한이 있습니다. 그리고 클래식 IDOR의 API 버전
(안전하지 않은 직접 개체 참조): 클라이언트가 묻습니다. /api/invoices/1234 그리고 서버
송장 1234가 요청한 사용자의 것인지 확인하지 않고 응답합니다.
// VULNERABILE: L'utente può leggere le fatture di qualsiasi cliente
app.get('/api/invoices/:id', authenticate, async (req, res) => {
// Manca la verifica che req.user.id == invoice.customerId
const invoice = await Invoice.findById(req.params.id);
res.json(invoice); // Restituisce qualsiasi fattura, a chiunque sia autenticato
});
// CORRETTO: Verifica sempre che la risorsa appartenga all'utente
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findOne({
_id: req.params.id,
customerId: req.user.id, // Filtra per proprietario nel query
});
if (!invoice) {
// Restituisci 404, non 403 (non rivelare l'esistenza della risorsa)
return res.status(404).json({ error: 'Invoice not found' });
}
res.json(invoice);
});
// Con TypeORM: verifica BOLA con query builder
const invoice = await invoiceRepository.findOne({
where: {
id: parseInt(req.params.id),
customer: { id: req.user.id }, // JOIN implicito con verifica ownership
},
});
// Pattern helper per verifiche BOLA sistematiche
async function findOwnedResource<T>(
model: Model<T>,
resourceId: string,
ownerId: string,
ownerField = 'userId'
): Promise<T | null> {
return model.findOne({
_id: resourceId,
[ownerField]: ownerId,
});
}
API2:2023 - 손상된 인증
손상된 인증은 단순한 로그인 부족 그 이상입니다. 알고리즘이 포함된 JWT 토큰 포함
none, 약한 서명 키, 대상 확인 부족(aud) 전자
발행자의 (iss), 만료되지 않는 토큰, 무차별 대입 보호 기능 부족
로그인 끝점에서. 다음으로 서명된 JWT를 발견한 공격자 HS256 그리고 하나
약한 키 secret 모든 역할은 재할당될 수 있습니다.
API3:2023 - BOPLA(깨진 개체 속성 수준 인증)
2023년에 새로 추가된 BOPLA는 이전의 두 가지 취약점을 결합합니다. 과도한 데이터 노출
(API는 민감한 데이터를 포함하여 필요한 것보다 더 많은 필드를 반환합니다) e 대량 할당
(API는 허용해서는 안 되는 필드를 허용하므로 사용자가 편집할 수 있습니다. isAdmin
o role). 입력과 출력 모두에서 보안 패턴과 필드의 명시적 투영입니다.
// VULNERABILE: Mass Assignment - l'utente può settare isAdmin
app.put('/api/users/:id', authenticate, async (req, res) => {
// req.body può contenere { name: 'John', isAdmin: true, role: 'superadmin' }
await User.findByIdAndUpdate(req.params.id, req.body); // Accetta tutto
});
// CORRETTO: Whitelist esplicita dei campi modificabili con Zod
import { z } from 'zod';
const UpdateUserSchema = z.object({
name: z.string().min(2).max(100).optional(),
email: z.string().email().optional(),
bio: z.string().max(500).optional(),
// isAdmin, role, permissions: NON presenti nello schema = non accettati
});
app.put('/api/users/:id', authenticate, async (req, res) => {
const parsed = UpdateUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
// Solo i campi validati vengono aggiornati
await User.findByIdAndUpdate(req.params.id, parsed.data);
res.json({ success: true });
});
// VULNERABILE: Excessive Data Exposure
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // Restituisce passwordHash, twoFactorSecret, internalNotes...
});
// CORRETTO: Proiezione esplicita - solo campi pubblici
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id)
.select('name email avatar bio createdAt -_id');
res.json(user);
});
API4:2023 - 무제한 리소스 소비
"리소스 부족 및 속도 제한"을 대체하는 이 범주는 다음의 모든 시나리오를 다룹니다. API가 리소스 소비를 제한하지 않는 경우: 수백만 달러를 반환하는 페이지 매김 없는 요청 레코드, 크기 제한 없는 파일 업로드, 깊이가 무제한인 GraphQL 쿼리, 웹후크 시간 초과 없이, 제한 없이 CPU 집약적인 작업을 수행할 수 있습니다. 속도 제한만으로는 충분하지 않습니다. 파이프라인의 모든 수준에서 제한이 필요합니다.
API5 - API10: 기타 심각한 취약점
- API5 - 손상된 기능 수준 인증: 일반 사용자가 액세스할 수 있는 관리 엔드포인트(종종 문서에 숨겨져 있지만 코드로 보호되지 않음)
- API6 - 민감한 비즈니스 흐름에 대한 무제한 액세스: 대량 계정 생성, 자동 구매, 대량 OTP 전송 등 적법한 흐름을 남용하는 행위
- API7 - 서버측 요청 위조(SSRF): API는 클라이언트가 제공한 URL을 수락하고 이를 서버측에서 요청하여 내부 서비스에 접근할 수 있도록 합니다.
- API8 - 잘못된 보안 구성: 누락된 헤더, 와일드카드 CORS, 프로덕션의 디버그 모드, 인증 없이 노출되는 사용되지 않는 API 버전
- API9 - 부적절한 재고 관리: 더 이상 사용되지 않는 API 버전이 제거되지 않음, 프로덕션에서 엔드포인트 테스트, 섀도우 API가 문서화되지 않음
- API10 - 안전하지 않은 API 소비: 출력 검증 및 오류 처리 없이 타사 API에 대한 맹목적인 신뢰
속도 제한: 알고리즘 및 구현
속도 제한은 요청 수를 제한하여 API 남용을 방지하는 메커니즘입니다. 클라이언트가 일정 기간 동안 만들 수 있는 것입니다. 이는 단순한 DDoS 방지 조치가 아닙니다. 크리덴셜 스터핑, 스크래핑, 리소스 열거 및 비즈니스 흐름 남용으로부터 보호할 수 있습니다. 선택 알고리즘의 영향은 사용자 경험과 효과적인 보호에 영향을 미칩니다.
토큰 버킷 알고리즘
토큰 버킷은 속도 제한을 위한 가장 일반적인 알고리즘입니다. 각 클라이언트에는 다음과 같은 "버킷"이 있습니다. 최대 용량 N 토큰. 토큰은 초당 R 토큰의 일정한 비율로 추가됩니다. 각 요청은 토큰을 사용합니다. 버킷이 비어 있으면 요청은 HTTP 429로 거부됩니다. 가능하다는 장점이 있습니다 트래픽 버스트 버킷의 용량까지, 그런 다음 초당 R 요청으로 안정화됩니다. 자연파 트래픽이 있는 공개 API에 이상적입니다.
// Token Bucket con Redis - implementazione production-ready
// npm install ioredis
import Redis from 'ioredis';
import { Request, Response, NextFunction } from 'express';
const redis = new Redis(process.env.REDIS_URL!);
interface TokenBucketConfig {
capacity: number; // capacità massima del secchio (burst max)
refillRate: number; // Token aggiunti per secondo (regime stazionario)
}
class TokenBucketLimiter {
constructor(private config: TokenBucketConfig) {}
async checkLimit(key: string): Promise<{
allowed: boolean;
remaining: number;
resetMs: number;
}> {
const now = Date.now();
const bucketKey = `rl:tb:${key}`;
// Lua script per operazione atomica - critico per evitare race condition
const luaScript = `
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1]) or capacity
local last_ts = tonumber(data[2]) or now
-- Calcola token da aggiungere in base al tempo trascorso
local elapsed_sec = (now - last_ts) / 1000.0
local refilled = math.min(capacity, tokens + elapsed_sec * refill_rate)
if refilled >= 1.0 then
local remaining = refilled - 1.0
redis.call('HMSET', KEYS[1], 'tokens', remaining, 'ts', now)
redis.call('PEXPIRE', KEYS[1], 3600000)
return {1, math.floor(remaining)}
else
redis.call('HMSET', KEYS[1], 'tokens', refilled, 'ts', now)
redis.call('PEXPIRE', KEYS[1], 3600000)
return {0, 0}
end
`;
const result = await redis.eval(
luaScript, 1, bucketKey,
this.config.capacity,
this.config.refillRate,
now
) as [number, number];
const resetMs = result[0] === 0
? Math.ceil((1 / this.config.refillRate) * 1000)
: 0;
return {
allowed: result[0] === 1,
remaining: result[1],
resetMs,
};
}
middleware() {
return async (req: Request, res: Response, next: NextFunction) => {
// Chiave per utente autenticato, per IP altrimenti
const key = (req as any).user?.id
? `user:${(req as any).user.id}`
: `ip:${req.ip}`;
const { allowed, remaining, resetMs } = await this.checkLimit(key);
// Headers standard RateLimit (RFC 6585 + draft-ietf-httpapi-ratelimit-headers)
res.setHeader('X-RateLimit-Limit', this.config.capacity);
res.setHeader('X-RateLimit-Remaining', remaining);
res.setHeader('X-RateLimit-Reset', Date.now() + resetMs);
res.setHeader('Retry-After', Math.ceil(resetMs / 1000));
if (!allowed) {
return res.status(429).json({
error: 'Too Many Requests',
message: `Rate limit exceeded. Retry after ${Math.ceil(resetMs / 1000)}s`,
retryAfter: Math.ceil(resetMs / 1000),
});
}
next();
};
}
}
// Limiter differenziati per endpoint con profili diversi
export const apiLimiter = new TokenBucketLimiter({
capacity: 100, // 100 richieste burst
refillRate: 10, // 10 req/s regime stazionario
});
export const authLimiter = new TokenBucketLimiter({
capacity: 5, // Solo 5 tentativi consecutivi (anti brute-force)
refillRate: 0.017, // ~1 token/minuto
});
export const uploadLimiter = new TokenBucketLimiter({
capacity: 10,
refillRate: 0.1, // 1 upload ogni 10 secondi
});
슬라이딩 윈도우 알고리즘
고정 창에는 알려진 문제가 있습니다. 공격자가 하나의 요청 끝에 N개의 요청을 집중할 수 있습니다. 창과 다음 요청이 시작될 때 N개의 요청이 발생하여 짧은 시간 내에 2N개의 요청이 발생합니다. 기술적인 한계를 위반하지 않고 시간을 단축할 수 있습니다. 슬라이딩 윈도우는 카운트를 유지하여 이 문제를 해결합니다. Redis 정렬 세트를 사용하여 시간에 따라 "흐르는" 각 시간 창에 대해 정확합니다. 요소에는 점수로 요청의 타임스탬프가 있습니다.
// Sliding Window Counter con Redis Sorted Set
class SlidingWindowLimiter {
constructor(
private limit: number, // Numero massimo richieste nella finestra
private windowMs: number // Dimensione finestra in millisecondi
) {}
async isAllowed(identifier: string): Promise<{
allowed: boolean;
count: number;
resetMs: number;
}> {
const now = Date.now();
const windowStart = now - this.windowMs;
const key = `rl:sw:${identifier}`;
// Pipeline Redis per operazioni atomiche in sequenza
const pipeline = redis.pipeline();
// 1. Rimuovi elementi fuori dalla finestra temporale
pipeline.zremrangebyscore(key, '-inf', windowStart);
// 2. Aggiungi la richiesta corrente (member unico con timestamp + random)
pipeline.zadd(key, now, `${now}:${Math.random().toString(36).slice(2)}`);
// 3. Conta le richieste nella finestra corrente
pipeline.zcard(key);
// 4. TTL automatico per non accumulare chiavi orfane
pipeline.pexpire(key, this.windowMs);
const results = await pipeline.exec();
const count = results![2][1] as number;
// Calcola quando la richiesta più vecchia uscira dalla finestra
const oldest = await redis.zrange(key, 0, 0, 'WITHSCORES');
const resetMs = oldest.length > 1
? Math.max(0, parseInt(oldest[1]) + this.windowMs - now)
: this.windowMs;
return {
allowed: count <= this.limit,
count,
resetMs,
};
}
}
// Factory per middleware Express con key generator personalizzabile
function slidingWindowMiddleware(
limit: number,
windowMs: number,
keyGen?: (req: Request) => string
) {
const limiter = new SlidingWindowLimiter(limit, windowMs);
return async (req: Request, res: Response, next: NextFunction) => {
const key = keyGen
? keyGen(req)
: ((req as any).user?.id ?? req.ip ?? 'anon');
const { allowed, count, resetMs } = await limiter.isAllowed(key);
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - count));
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + resetMs).toISOString());
if (!allowed) {
return res.status(429).json({
error: 'Too Many Requests',
retryAfter: Math.ceil(resetMs / 1000),
});
}
next();
};
}
// Utilizzo: rate limit globale + specifici per endpoint sensibili
app.use('/api/', slidingWindowMiddleware(1000, 60_000)); // 1000/min globale
app.use('/api/auth/', slidingWindowMiddleware(10, 15 * 60_000)); // 10/15min per auth
app.post(
'/api/payments',
slidingWindowMiddleware(
5, 60 * 60_000,
(req) => `pay:${(req as any).user!.id}` // Per-user invece che per-IP
)
);
토큰 버킷과 슬라이딩 윈도우: 선택 가이드
| 표준 | 토큰 버킷 | 슬라이딩 윈도우 |
|---|---|---|
| 트래픽 버스트 | 버킷 용량까지 버스트 허용 | 균일 한도 적용, 버스트 없음 |
| 한계 정확도 | 대략적인 금액(리필 비율에 따라 다름) | 밀리초 단위까지 정확함 |
| 스토리지 Redis | 키당 값 2개(경량 해시) | 키당 N개 요소(정렬된 세트, 트래픽에 비례) |
| 최고 행동 | 오류 없이 자연스러운 피크를 흡수합니다. | 어려움: 한계를 초과하면 429가 반환됩니다. |
| 이상적인 사용 사례 | 공개 API, CDN, 가변 사용자 트래픽 | 중요한 엔드포인트: 인증, 결제, OTP |
OAuth 2.1 및 JWT: 최신 API 인증
OAuth 2.1(RFC 초안)은 OAuth 2.0의 보안 모범 사례를 통합합니다. 주요 차이점 OAuth 2.0과 비교하면 다음과 같습니다. 모든 클라이언트에 필수 PKCE (공공뿐만 아니라 클라이언트), 암시적 흐름이 제거됨, 자원 소유자 비밀번호 자격 증명 흐름이 제거되었습니다., 그리고 정확한 리디렉션 URI 일치 필수(아무것도 없음 와일드카드와 패턴 일치). OAuth 2.0 구현이 이미 OWASP 보안을 따른 경우 치트 시트, OAuth 2.1로 이동하려면 최소한의 변경이 필요합니다.
세분화된 범위: API에 대한 최소 권한
OAuth 범위는 토큰이 수행할 수 있는 작업을 정의합니다. 범위가 너무 넓음 read:all
o admin 최소 권한의 원칙을 위반합니다. 범위가 있는 도난당한 토큰
invoices:read:own 있는 것보다 훨씬 더 제한적인 피해를 입힙니다.
read:all. 범위의 세분성은 API의 실제 작업을 반영해야 합니다.
// Definizione scopes granulari per API di fatturazione
const SCOPES = {
// Formato: risorsa:operazione:contesto
'invoices:read:own': 'Leggere le proprie fatture',
'invoices:read:all': 'Leggere tutte le fatture (solo admin)',
'invoices:create': 'Creare nuove fatture',
'invoices:update:own': 'Modificare le proprie fatture',
'invoices:delete:own': 'Eliminare le proprie fatture',
'customers:read:own': 'Leggere il proprio profilo cliente',
'customers:read:all': 'Leggere tutti i clienti (solo admin)',
'reports:read:financial': 'Accedere ai report finanziari',
'payments:create': 'Effettuare pagamenti',
'webhooks:manage': 'Gestire i webhook',
} as const;
type Scope = keyof typeof SCOPES;
// Middleware scope check
function requireScope(...requiredScopes: Scope[]) {
return (req: Request, res: Response, next: NextFunction) => {
const tokenScopes: string[] = ((req as any).user?.scope ?? '').split(' ');
const hasScope = requiredScopes.every(s => tokenScopes.includes(s));
if (!hasScope) {
return res.status(403).json({
error: 'insufficient_scope',
required: requiredScopes,
granted: tokenScopes,
});
}
next();
};
}
// Route con scope checking granulare
app.get('/api/invoices',
authenticate,
requireScope('invoices:read:own'),
async (req, res) => {
const invoices = await Invoice.find({ userId: (req as any).user.id });
res.json(invoices);
}
);
app.get('/api/admin/invoices',
authenticate,
requireScope('invoices:read:all'), // Scope admin separato
async (req, res) => {
const invoices = await Invoice.find({});
res.json(invoices);
}
);
// Scope multipli richiesti contemporaneamente
app.post('/api/reports/financial',
authenticate,
requireScope('reports:read:financial', 'invoices:read:all'),
async (req, res) => {
// Richiede ENTRAMBI gli scopes
}
);
JWT: 6가지 치명적인 실수
JWT는 널리 사용되지만 똑같이 광범위하게 제대로 구현되지 않습니다. 이 여섯 가지 잘못된 패턴 강력한 인증 메커니즘을 심각한 취약점으로 전환합니다. 모든 포인트 문서화된 CVE 또는 실제 공격 패턴에 해당합니다.
// ERRORE 1: Accettare l'algoritmo 'none'
// CVE-2015-9235 - Molte librerie JWT pre-2016 lo accettavano
// Un attaccante modifica header.alg = "none" e rimuove la firma
// VULNERABILE:
const decoded = jwt.verify(token, secret); // Default: accetta tutti gli algoritmi
// CORRETTO: Whitelist esplicita degli algoritmi
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Solo asimmetrico RS256 per sistemi distribuiti
// oppure ['HS256'] con chiave forte (min 256 bit) per sistemi single-server
});
// ERRORE 2: Chiave simmetrica debole
// VULNERABILE: Brute-forceable in pochi secondi
const token = jwt.sign(payload, 'secret');
const token2 = jwt.sign(payload, 'mysecretkey123');
// CORRETTO: Usa RS256 (asimmetrico) oppure HS256 con chiave forte
// Genera chiave RS256:
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem
import fs from 'fs';
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
// ERRORE 3: Nessuna validazione claims iss/aud
// Un token emesso per un altro servizio potrebbe essere accettato
// CORRETTO: Verifica issuer e audience
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.myapp.com', // Chi ha emesso il token
audience: 'https://api.myapp.com', // Per chi e il token
// exp verificato automaticamente dalla libreria
});
// ERRORE 4: Access token con durata eccessiva
// VULNERABILE: Token valido 30 giorni = finestra di attacco enorme
const token = jwt.sign(payload, secret, { expiresIn: '30d' });
// CORRETTO: Access token breve (15min) + refresh token
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
scope: user.scopes.join(' '),
jti: crypto.randomUUID(), // JWT ID univoco per blacklisting
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
}
);
// Refresh token: opaco, salvato nel DB, revocabile
const refreshToken = crypto.randomBytes(32).toString('base64url');
await RefreshToken.create({
token: await bcrypt.hash(refreshToken, 12), // Hash nel DB, mai in chiaro
userId: user.id,
jti: crypto.randomUUID(),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
userAgent: req.get('User-Agent') ?? '',
ipAddress: req.ip ?? '',
});
// ERRORE 5: Refresh token senza rotazione
// Rotation: ogni uso genera un nuovo refresh token, il vecchio viene invalidato
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
// Cerca token attivi per l'utente
const stored = await RefreshToken.findOne({
userId: (req as any).session?.userId,
used: false,
expiresAt: { $gt: new Date() },
});
if (!stored || !(await bcrypt.compare(refreshToken, stored.token))) {
// Token non valido o già usato: REVOCA TUTTO (possibile furto)
await RefreshToken.deleteMany({ userId: stored?.userId });
return res.status(401).json({ error: 'Invalid refresh token' });
}
await stored.updateOne({ used: true }); // Invalida il vecchio
// Emetti nuovi token
const newAccessToken = generateAccessToken(stored.userId);
const newRefreshToken = crypto.randomBytes(32).toString('base64url');
await RefreshToken.create({
token: await bcrypt.hash(newRefreshToken, 12),
userId: stored.userId,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});
// Refresh token via HttpOnly cookie (non nel body)
res.cookie('rt', newRefreshToken, {
httpOnly: true, secure: true, sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000,
path: '/api/auth/refresh',
});
res.json({ accessToken: newAccessToken });
});
// ERRORE 6: JWT in localStorage
// localStorage e accessibile da qualsiasi script JS sulla pagina (XSS)
// CORRETTO: access token in-memory, refresh token in HttpOnly cookie
// Vedi sezione Angular checklist per l'implementazione lato client
API 키: 서버 간 통합을 위한 보안 관리
API 키는 클라이언트가 있는 기계 간 통신에 적합한 선택입니다. 계정 소유자가 제어합니다(자동화 스크립트, 타사 백엔드, CI/CD 통합). OAuth보다 간단하지만 세심한 관리가 필요합니다: API 키 이는 비밀번호처럼 취급되어야 하며 로그나 소스 코드에 노출되지 않아야 합니다.
// Gestione sicura API Keys - pattern ispirato a Stripe
import crypto from 'crypto';
import { timingSafeEqual } from 'crypto';
// Formato con prefisso identificativo del tipo di chiave
// sk_live_xxxx = production, sk_test_xxxx = sandbox
function generateApiKey(env: 'live' | 'test' = 'live'): {
key: string;
hash: string;
prefix: string;
} {
const random = crypto.randomBytes(24).toString('base64url');
const key = `sk_${env}_${random}`;
const prefix = key.slice(0, 10) + '...'; // Per display nell'UI
// SHA-256 del token: se il DB viene compromesso, le chiavi sono al sicuro
const hash = crypto.createHash('sha256').update(key).digest('hex');
return { key, hash, prefix };
}
// Autenticazione via API key con timing-safe comparison
async function authenticateApiKey(req: Request, res: Response, next: NextFunction) {
const rawKey =
req.headers['x-api-key'] as string ??
req.headers.authorization?.replace('Bearer ', '');
if (!rawKey) {
return res.status(401).json({ error: 'API key required' });
}
const incomingHash = crypto.createHash('sha256').update(rawKey).digest('hex');
// Cerca per hash nel DB
const storedKey = await ApiKey.findOne({
hash: incomingHash,
active: true,
expiresAt: { $gt: new Date() },
}).populate('owner');
if (!storedKey) {
// Timing-safe: evita timing attack per dedurre chiavi valide
const fakeBuf = Buffer.alloc(32, 0);
timingSafeEqual(
Buffer.from(incomingHash, 'hex'),
fakeBuf
);
return res.status(401).json({ error: 'Invalid or expired API key' });
}
// Aggiorna metadati per auditing
await ApiKey.findByIdAndUpdate(storedKey._id, {
lastUsedAt: new Date(),
$inc: { requestCount: 1 },
lastUsedIp: req.ip,
});
(req as any).user = storedKey.owner;
(req as any).apiKey = storedKey; // Dati chiave per rate limiting specifico
next();
}
// Endpoint creazione chiave con scopes e rate limit personalizzato
app.post('/api/keys', authenticate, async (req, res) => {
const schema = z.object({
name: z.string().min(1).max(100),
scopes: z.array(z.string()).min(1),
expiresInDays: z.number().int().min(1).max(365).default(90),
rateLimit: z.number().int().min(10).max(10000).default(1000),
environment: z.enum(['live', 'test']).default('live'),
});
const parsed = schema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
const { key, hash, prefix } = generateApiKey(parsed.data.environment);
await ApiKey.create({
name: parsed.data.name,
hash, // Solo hash nel DB, MAI la chiave in chiaro
prefix, // Per identificazione nell'UI senza esporre la chiave
ownerId: (req as any).user.id,
scopes: parsed.data.scopes,
rateLimit: parsed.data.rateLimit,
expiresAt: new Date(Date.now() + parsed.data.expiresInDays * 86_400_000),
});
// Restituisce la chiave UNA SOLA VOLTA (pattern Stripe/GitHub)
res.status(201).json({
key, // Solo in questo momento, non più recuperabile
prefix, // Per display futuro
message: 'Store this key securely. It will not be shown again.',
});
});
Zod를 사용한 입력 검증
입력 검증은 주입, 대량 할당 및 예상치 못한 행동. TypeScript API의 경우 조드 2025년의 참조 도구: 스키마를 한 번 정의하면 런타임 유효성 검사와 추론된 TypeScript 유형을 얻을 수 있습니다. TypeScript 인터페이스와 런타임 유효성 검사기 간의 중복 없이 자동으로. 모든 엔드포인트 본문, 매개변수 및 쿼리 문자열을 별도로 검증해야 합니다.
// Input validation completa con Zod
// npm install zod
import { z } from 'zod';
// Schema con regole di sicurezza specifiche
const CreateOrderSchema = z.object({
// UUID format: previene injection via ID malformati
customerId: z.string().uuid('ID cliente non valido'),
// Limiti di lunghezza espliciti: previene DoS via stringhe enormi
notes: z.string().max(1000).optional().transform(v => v?.trim()),
// Array con limiti: previene mass insertion
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100), // Limiti di business
unitPrice: z.number().positive().max(99_999.99),
})).min(1).max(50),
// Enum: solo valori predefiniti, no stringhe arbitrarie
shippingMethod: z.enum(['standard', 'express', 'overnight']),
// Data con validazione semantica
requestedDelivery: z.string().datetime().transform(v => new Date(v))
.refine(d => d > new Date(), 'La data deve essere nel futuro')
.optional(),
// URL con vincolo HTTPS
webhookUrl: z.string().url()
.refine(u => u.startsWith('https://'), 'URL deve usare HTTPS')
.optional(),
});
type CreateOrderInput = z.infer<typeof CreateOrderSchema>; // Tipo inferito automaticamente
// Middleware di validazione riutilizzabile
function validateBody<T extends z.ZodType>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
fieldErrors: result.error.flatten().fieldErrors,
});
}
req.body = result.data; // Sostituisci con dati validati e trasformati
next();
};
}
function validateQuery<T extends z.ZodType>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.query);
if (!result.success) {
return res.status(400).json({ error: 'Invalid query parameters' });
}
(req as any).validatedQuery = result.data;
next();
};
}
// Schema per paginazione sicura
const PaginationSchema = z.object({
page: z.coerce.number().int().min(1).max(10_000).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
order: z.enum(['asc', 'desc']).default('desc'),
// Ricerca testuale con sanitizzazione
q: z.string().max(100).optional().transform(v => v?.replace(/[^\w\s-]/g, '')),
});
// Utilizzo nella route
app.post(
'/api/orders',
authenticate,
requireScope('orders:create'),
apiLimiter.middleware(),
validateBody(CreateOrderSchema),
async (req: Request, res: Response) => {
const body = req.body as CreateOrderInput; // Tipizzato e validato
const order = await OrderService.create(body, (req as any).user.id);
res.status(201).json(order);
}
);
안전한 CORS 구성
CORS는 개발자가 프런트엔드에서 API를 호출할 때 처음으로 터치하는 구성인 경우가 많습니다.
그들은 개발에 실패합니다. 가장 일반적이고 잘못된 대답은 다음과 같습니다. Access-Control-Allow-Origin: *.
이렇게 하면 전체 API에 대한 동일 출처 보호가 비활성화됩니다. 더 나쁜 것은,
Access-Control-Allow-Origin: * ~와 함께 credentials: true 전혀 작동하지 않습니다
최신 브라우저의 경우(보안용) 코드 검토 시 즉시 경고를 발생시켜야 합니다.
// Configurazione CORS sicura con whitelist dinamica
// npm install cors @types/cors
import cors from 'cors';
// Whitelist separata per ambiente: mai mescolare dev e produzione
const PROD_ORIGINS = [
'https://app.mycompany.com',
'https://admin.mycompany.com',
];
const DEV_ORIGINS = [
'http://localhost:4200', // Angular CLI dev server
'http://localhost:3000',
...PROD_ORIGINS,
];
const allowedOrigins =
process.env.NODE_ENV === 'production' ? PROD_ORIGINS : DEV_ORIGINS;
const corsOptions: cors.CorsOptions = {
origin: (origin, callback) => {
// Permetti richieste senza Origin header (curl, Postman, server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS: origine '${origin}' non autorizzata`));
}
},
credentials: true, // Necessario per cookie cross-origin (refresh token)
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-API-Key',
'X-Correlation-ID',
'X-Requested-With',
],
// Headers che il client può leggere dalla risposta
exposedHeaders: [
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
'X-RateLimit-Reset',
'X-Correlation-ID',
],
maxAge: 86_400, // Cache preflight OPTIONS per 24 ore
};
app.use(cors(corsOptions));
// Gestione esplicita degli errori CORS
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err.message.startsWith('CORS:')) {
return res.status(403).json({
error: 'CORS Policy Violation',
message: 'Origin not allowed',
});
}
next(err);
});
// Security headers con helmet
import helmet from 'helmet';
app.use(helmet({
// Previeni MIME-type sniffing
noSniff: true,
// Rimuovi X-Powered-By (fingerprinting)
hidePoweredBy: true,
// HSTS: forza HTTPS per 1 anno
hsts: {
maxAge: 31_536_000,
includeSubDomains: true,
preload: true,
},
// Limita Referrer nelle richieste cross-origin
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
보안을 위한 API 게이트웨이 패턴
API 게이트웨이는 인증, 속도 제한, 로깅과 같은 교차 문제를 중앙 집중화합니다. 및 요청 처리. 마이크로서비스 아키텍처에서는 논리 복제를 피하세요. 모든 서비스의 안전. Express 미들웨어의 구성은 특정 순서를 따라야 합니다. 각 처리 전 보안 헤더, 속도 제한, 인증, 유효성 검사, 드디어 노선.
// API Gateway middleware stack - ordine critico per la sicurezza
import express from 'express';
import helmet from 'helmet';
import { v4 as uuidv4 } from 'uuid';
const app = express();
// STEP 1: Correlation ID - traccia ogni richiesta end-to-end
app.use((req: Request, res: Response, next: NextFunction) => {
const cid = (req.headers['x-correlation-id'] as string) ?? uuidv4();
(req as any).correlationId = cid;
res.setHeader('X-Correlation-ID', cid);
next();
});
// STEP 2: Security headers
app.use(helmet());
app.use(cors(corsOptions));
// STEP 3: Body parsing con limiti (previene DoS via payload enormi)
app.use(express.json({
limit: '10kb', // 10KB max per JSON - adatta per upload separati
strict: true, // Solo array/oggetti JSON validi
}));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
// STEP 4: Rate limiting globale (prima dell'autenticazione)
app.use('/api/', slidingWindowMiddleware(5000, 60_000)); // 5000 req/min per IP
// STEP 5: Autenticazione (dopo rate limit - non sprecare risorse su token validazione)
app.use('/api/v1/', authenticateRequest);
// STEP 6: Rate limiting per utente autenticato (dopo auth per avere user ID)
app.use('/api/v1/auth/', slidingWindowMiddleware(
10, 15 * 60_000,
(req) => `auth:${req.ip}`
));
// STEP 7: Routes applicative
app.use('/api/v1/', v1Router);
// STEP 8: Error handler sicuro - SEMPRE in fondo
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const correlationId = (req as any).correlationId;
// Log interno dettagliato (solo per team di sviluppo)
console.error({
correlationId,
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
userId: (req as any).user?.id ?? 'anonymous',
});
// Risposta esterna: nessun dettaglio interno in produzione
if (process.env.NODE_ENV === 'production') {
res.status(500).json({
error: 'Internal Server Error',
correlationId, // Per il supporto: consente di trovare il log corrispondente
});
} else {
res.status(500).json({
error: err.message,
correlationId,
});
}
});
// Circuit breaker per servizi upstream - evita cascade failures
import CircuitBreaker from 'opossum';
const paymentBreaker = new CircuitBreaker(
async (payload: unknown) => {
const resp = await fetch('https://payment-svc/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3000), // 3s timeout
});
if (!resp.ok) throw new Error(`Payment service error: ${resp.status}`);
return resp.json();
},
{
timeout: 3000,
errorThresholdPercentage: 50, // Apre il circuito se 50% delle chiamate fallisce
resetTimeout: 30_000, // Riprova dopo 30s
volumeThreshold: 5, // Minimo 5 chiamate prima di aprire
}
);
paymentBreaker.fallback(() => {
throw new Error('Payment service temporarily unavailable');
});
모니터링: 실시간 공격 탐지
효과적인 API 모니터링 시스템은 특정 보안 지표에 중점을 둡니다. IP 및 엔드포인트당 401/403 오류율, 리소스 ID의 열거 패턴, 페이로드 크기에 대한 비정상적인 요청, 문서화되지 않았거나 더 이상 사용되지 않는 엔드포인트에 대한 액세스. 일반 애플리케이션 지표로는 충분하지 않습니다. 안전 지향 카운터가 필요합니다.
// Monitoring sicurezza API con metriche orientate agli attacchi
// npm install prom-client
import { Counter, Histogram, register } from 'prom-client';
const authFailureCounter = new Counter({
name: 'api_auth_failures_total',
help: 'Numero totale di fallimenti autenticazione',
labelNames: ['failure_type', 'endpoint'],
});
const rateLimitCounter = new Counter({
name: 'api_rate_limit_hits_total',
help: 'Rate limit superato',
labelNames: ['endpoint', 'identifier_type'],
});
const suspiciousActivityCounter = new Counter({
name: 'api_suspicious_activity_total',
help: 'Attivita potenzialmente sospette rilevate',
labelNames: ['type', 'severity'],
});
// Rilevamento pattern di enumerazione in memoria
// In produzione usa Redis per coordinare più istanze
const enumTracker = new Map<string, { count: number; firstSeen: number }>();
export async function detectEnumeration(
ip: string,
endpoint: string,
statusCode: number
): Promise<void> {
if (statusCode !== 404 && statusCode !== 403) return;
const key = `${ip}:${endpoint.split('/')[2] ?? 'root'}`; // Raggruppa per risorsa
const now = Date.now();
const WINDOW = 60_000; // 1 minuto
const existing = enumTracker.get(key);
if (!existing || now - existing.firstSeen > WINDOW) {
enumTracker.set(key, { count: 1, firstSeen: now });
return;
}
existing.count++;
if (existing.count >= 20) {
suspiciousActivityCounter.inc({
type: 'enumeration_attempt',
severity: existing.count >= 50 ? 'critical' : 'high',
});
if (existing.count === 20) {
// Blocca IP per 1 ora
await redis.setex(`blocked:${ip}`, 3600, '1');
console.warn(`[SECURITY] Enumeration detected from ${ip}, endpoint: ${endpoint}`);
}
}
}
// Middleware raccolta metriche
app.use((req: Request, res: Response, next: NextFunction) => {
res.on('finish', () => {
const endpoint = (req.route?.path ?? req.path).replace(/\/\d+/g, '/:id');
if (res.statusCode === 401) {
authFailureCounter.inc({ failure_type: 'invalid_token', endpoint });
} else if (res.statusCode === 403) {
authFailureCounter.inc({ failure_type: 'forbidden', endpoint });
} else if (res.statusCode === 429) {
const identType = (req as any).user ? 'user' : 'ip';
rateLimitCounter.inc({ endpoint, identifier_type: identType });
}
// Rileva pattern sospetti asincrono (non blocca la risposta)
detectEnumeration(req.ip ?? '', req.path, res.statusCode).catch(console.error);
});
next();
});
// Endpoint metriche (solo per monitoring interno - protetto)
app.get('/internal/metrics', authenticateInternal, async (_, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
API 보안을 위한 Angular 체크리스트
Angular 프런트엔드에는 API 보안에 대한 특정 책임이 있습니다. 어디의 선택 토큰을 저장하면 XSS 공격 표면에 직접적인 영향을 미칩니다. HTTP 인터셉터 수십 개의 서비스에 중요한 논리가 분산되는 것을 방지하여 보안 관리를 중앙 집중화합니다.
// HTTP Interceptor Angular per sicurezza API
// src/app/interceptors/api-security.interceptor.ts
import { inject } from '@angular/core';
import {
HttpInterceptorFn,
HttpRequest,
HttpHandlerFn,
HttpErrorResponse,
} from '@angular/common/http';
import { catchError, switchMap, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const apiSecurityInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
) => {
const auth = inject(AuthService);
const router = inject(Router);
// Aggiungi token solo per le nostre API (non per CDN o API di terze parti)
const isOurApi = req.url.startsWith('/api') ||
req.url.startsWith('https://api.myapp.com');
const accessToken = isOurApi ? auth.getAccessToken() : null;
const secureReq = accessToken
? req.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`,
'X-Correlation-ID': crypto.randomUUID(),
},
// Invia cookie HttpOnly (refresh token) solo per le nostre API
withCredentials: isOurApi,
})
: req;
return next(secureReq).pipe(
catchError((error: HttpErrorResponse) => {
switch (error.status) {
case 401:
// Token scaduto: tenta refresh automatico una volta
return auth.refreshToken().pipe(
switchMap((newToken: string) =>
next(req.clone({
setHeaders: { Authorization: `Bearer ${newToken}` },
withCredentials: true,
}))
),
catchError(() => {
auth.logout();
router.navigate(['/login'], {
queryParams: { reason: 'session_expired' },
});
return throwError(() => error);
})
);
case 403:
router.navigate(['/forbidden']);
return throwError(() => error);
case 429:
// Non loggare come errore critico: e il rate limiter che funziona
const retryAfter = error.headers.get('Retry-After');
console.warn(`Rate limited. Riprova tra ${retryAfter}s`);
return throwError(() => error);
default:
return throwError(() => error);
}
})
);
};
// AuthService con access token in-memory (resistente a XSS)
import { Injectable, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, tap, map } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
// CRITICO: In-memory, non localStorage/sessionStorage
// Perso al refresh della pagina (feature, non bug: forza re-auth)
private readonly _accessToken = signal<string | null>(null);
readonly isAuthenticated = signal<boolean>(false);
getAccessToken(): string | null {
return this._accessToken();
}
login(email: string, password: string): Observable<void> {
return this.http.post<{ accessToken: string }>(
'/api/auth/login',
{ email, password },
{ withCredentials: true } // Riceve HttpOnly cookie con refresh token
).pipe(
tap(res => {
this._accessToken.set(res.accessToken);
this.isAuthenticated.set(true);
}),
map(() => void 0)
);
}
refreshToken(): Observable<string> {
return this.http.post<{ accessToken: string }>(
'/api/auth/refresh', {},
{ withCredentials: true } // Invia HttpOnly cookie automaticamente
).pipe(
tap(res => this._accessToken.set(res.accessToken)),
map(res => res.accessToken)
);
}
logout(): void {
this._accessToken.set(null);
this.isAuthenticated.set(false);
// Invalida il refresh token lato server
this.http.post('/api/auth/logout', {}, { withCredentials: true })
.subscribe();
}
}
// Registrazione interceptor in app.config.ts
// provideHttpClient(withInterceptors([apiSecurityInterceptor]))
API 보안 체크리스트 - Angular + Node.js
| 영역 | 확인하다 | 우선 사항 |
|---|---|---|
| 객체 승인 | BOLA 확인: 각 쿼리는 데이터를 반환하기 전에 소유자별로 필터링합니다. | 비판 |
| JWT | RS256 알고리즘, 15분 만료, iss/aud/exp 청구 확인됨 | 비판 |
| 토큰 보관 | 메모리 내 액세스 토큰 Angular, HttpOnly 쿠키의 새로 고침 토큰 SameSite=Strict | 비판 |
| 속도 제한 | 일반 API용 토큰 버킷, 인증/결제용 슬라이딩 윈도우 | 높은 |
| 입력 검증 | 본문/매개변수/쿼리의 Zod, 길이 제한, 기본값 열거형 | 높은 |
| 코르스 | 명시적 출처 화이트리스트, 자격 증명이 포함된 와일드카드 없음 | 높은 |
| 범위 OAuth | 리소스:작업:컨텍스트별로 세분화되어 각 엔드포인트에서 확인됨 | 높은 |
| 오류 처리 | 프로덕션에는 스택 추적이 없으며 지원을 위한 상관 ID | 평균 |
| 모니터링 | 401/403/429 카운터, 열거 감지, 이상 징후 경고 | 평균 |
| API 키 | DB에는 해시만 있고 일반 해시는 없습니다. 타이밍에 안전한 비교; 식별용 접두사 | 평균 |
절대 피해야 할 안티 패턴
- localStorage의 JWT: 페이지의 모든 JS 스크립트에서 액세스할 수 있습니다. 액세스 토큰에는 인메모리를 사용하고 새로 고침 토큰에는 HttpOnly 쿠키를 사용합니다.
- 자격 증명이 포함된 CORS 와일드카드:
Access-Control-Allow-Origin: *~와 함께credentials: true설계상 최신 브라우저에서는 거부됩니다. 구성이 잘못되었음을 나타냅니다. - IP 전용 속도 제한: 봇넷은 수천 개의 IP에 공격을 분산시킵니다. IP당 속도 제한과 사용자 ID당 속도 제한 결합
- 범위가 너무 넓음:
adminoread:all그들은 사소한 특권을 침해합니다. 범위가 있는 도난당한 토큰invoices:read:own훨씬 적은 피해를 입힙니다 - 프로덕션에 노출된 스택 추적: 내부 경로, 라이브러리 버전, DB 구조를 공개합니다. 항상 프로덕션의 세부 사항을 모호하게 만드는 오류 처리기를 사용하세요.
- 지정되지 않은 JWT 알고리즘: 항상 지정
algorithms: ['RS256']. 기본 구성을 사용하는 일부 JWT 라이브러리는 알고리즘을 허용합니다.none - BOLA가 확인되지 않음: API 공격의 40%가 BOLA를 악용합니다. ID를 수락하는 각 엔드포인트는 사용자가 리소스의 소유자인지 확인해야 합니다.
결론
API 보안은 단일 도구나 구성으로 해결되지 않습니다. 연습이에요 모든 수준에서 주의가 필요한 계속: 엔드포인트 설계(BOLA 확인) 체계적으로 세분화된 범위), 속도 제한 구현(알고리즘 선택) 각 사용 사례에 적합), OAuth 2.1 및 JWT(RS256, 짧은 기한, 토큰 순환 새로 고침), 공격 패턴이 피해를 입히기 전에 탐지하기 위한 모니터링까지 가능합니다.
2024~2025년 데이터에 따르면 API 사고의 대부분은 취약점과 관련이 없습니다. 이국적인: 기본 권한 오류에 관한 것입니다(리소스가 속해 있는지 확인하지 않음). 요청한 사용자에게), 허용적인 CORS 구성, 만료 및 부재 없는 JWT 토큰 인증 엔드포인트의 속도 제한. 잘 알려진 패턴으로 해결할 수 있는 문제입니다. 규율을 적용했습니다.
AI 지원 코딩 도구를 사용하는 경우 다음 사항을 기억하세요. AI가 생성한 코드의 45% 보안 테스트에 실패: 생성된 코드는 인증을 구현하지만 생략하는 경우가 많습니다. BOLA(리소스 소유권 확인)는 IP만을 기반으로 하는 표면적 속도 제한을 사용합니다. 유효성 검사(대량 할당) 없이 임의의 필드를 입력으로 허용합니다. 이 체크리스트를 사용하세요 기사를 체계적으로 검증하는 지점으로 삼았습니다.
웹 보안 시리즈의 다음 단계
- 이전 기사: 보안 인증: 세션, 쿠키 및 최신 ID - 세션 관리, 보안 쿠키, OAuth 2.1 PKCE 및 WebAuthn
- 다음 기사: 공급망 보안: npm 감사 및 SBOM - npm 감사, dependencyabot, SBOM 생성 및 보안 종속성 관리
- 기초: 2025년 OWASP 상위 10위 - A03 공급망 및 A10 오류 처리를 포함한 가장 중요한 웹 취약점에 대한 전체 개요
- 관련 DevOps: 시리즈를 확인해보세요 DevOps 프론트엔드 API 보안을 CI/CD 파이프라인에 통합하기 위해
- AI 및 보안: 시리즈 보기 바이브코딩 AI 생성 코드의 위험과 이를 체계적으로 테스트하는 방법을 이해합니다.







