[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
---
|
||||
id: security-owasp-api-top10
|
||||
title: OWASP API Top 10 — modern API security
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [security, api, vibe-coding]
|
||||
tech_stack: { language: "TS", applicable_to: ["Backend", "Security"] }
|
||||
applied_in: []
|
||||
aliases: [OWASP API, BOLA, broken authorization, mass assignment, API security, 2023]
|
||||
---
|
||||
|
||||
# OWASP API Top 10 (2023)
|
||||
|
||||
> API 의 가장 흔한 vulnerability. **BOLA, broken auth, mass assignment, ...**.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- 매 endpoint 가 attack surface.
|
||||
- Web OWASP 와 다름 (API-specific).
|
||||
- Access control 가 가장 흔한 problem.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### API1: BOLA (Broken Object Level Authorization)
|
||||
```ts
|
||||
// ❌ User A 가 User B 의 data 가져옴
|
||||
app.get('/api/users/:id', (req, res) => {
|
||||
const user = await db.users.findById(req.params.id);
|
||||
res.json(user);
|
||||
});
|
||||
|
||||
// ✅ Authorization check
|
||||
app.get('/api/users/:id', (req, res) => {
|
||||
const user = await db.users.findById(req.params.id);
|
||||
if (user.id !== req.user.id && !req.user.isAdmin) {
|
||||
return res.status(403).json({ error: 'forbidden' });
|
||||
}
|
||||
res.json(user);
|
||||
});
|
||||
```
|
||||
|
||||
→ 가장 흔한 API bug. Object 의 ownership 검증.
|
||||
|
||||
### API2: Broken Authentication
|
||||
```ts
|
||||
// ❌ Weak password / no rate limit
|
||||
app.post('/login', async (req, res) => {
|
||||
const user = await db.users.findOne({ email: req.body.email });
|
||||
if (user && user.password === req.body.password) {
|
||||
return res.json({ token: '...' });
|
||||
}
|
||||
});
|
||||
|
||||
// ✅
|
||||
app.post('/login', rateLimit({ max: 5 }), async (req, res) => {
|
||||
const user = await db.users.findOne({ email: req.body.email });
|
||||
if (user && await bcrypt.compare(req.body.password, user.passwordHash)) {
|
||||
return res.json({ token: signJWT(user) });
|
||||
}
|
||||
await sleep(1000); // timing attack 방지
|
||||
res.status(401).json({ error: 'invalid' });
|
||||
});
|
||||
```
|
||||
|
||||
→ Bcrypt + rate limit + 시간 const.
|
||||
|
||||
### API3: BOPLA (Broken Object Property Level Authorization)
|
||||
```ts
|
||||
// ❌ User 가 자기 role 변경
|
||||
app.patch('/api/users/:id', async (req, res) => {
|
||||
await db.users.update(req.params.id, req.body);
|
||||
});
|
||||
// → req.body 가 { role: 'admin' } 일 수.
|
||||
|
||||
// ✅ Whitelist
|
||||
const ALLOWED = ['name', 'email', 'avatar'];
|
||||
const update = pick(req.body, ALLOWED);
|
||||
await db.users.update(req.params.id, update);
|
||||
```
|
||||
|
||||
→ Mass assignment 방지.
|
||||
|
||||
### API4: Unrestricted Resource Consumption
|
||||
```ts
|
||||
// ❌ 무한 result
|
||||
app.get('/api/items', async (req, res) => {
|
||||
const items = await db.items.find({}); // 매 item.
|
||||
res.json(items);
|
||||
});
|
||||
|
||||
// ✅ Pagination + limit
|
||||
app.get('/api/items', async (req, res) => {
|
||||
const limit = Math.min(Number(req.query.limit) || 50, 100);
|
||||
const items = await db.items.find({}).limit(limit);
|
||||
res.json(items);
|
||||
});
|
||||
```
|
||||
|
||||
### API5: Broken Function Level Authorization
|
||||
```ts
|
||||
// ❌ Admin endpoint 가 admin check 안
|
||||
app.delete('/api/users/:id', (req, res) => {
|
||||
await db.users.delete(req.params.id);
|
||||
});
|
||||
|
||||
// ✅
|
||||
app.delete('/api/users/:id', requireAdmin, (req, res) => {
|
||||
await db.users.delete(req.params.id);
|
||||
});
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.user?.isAdmin) return res.status(403).end();
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
### API6: Unrestricted Access to Sensitive Business Flows
|
||||
```ts
|
||||
// ❌ 1 user 가 매 product 의 1 차 buy.
|
||||
// → Reseller bot 가 모두 buy.
|
||||
|
||||
// ✅ Per-user limit + CAPTCHA + behavior detect
|
||||
if (await purchaseCount(req.user.id, productId, '1h') > 1) {
|
||||
return res.status(429).end();
|
||||
}
|
||||
```
|
||||
|
||||
→ Business logic abuse.
|
||||
|
||||
### API7: Server-Side Request Forgery (SSRF)
|
||||
```ts
|
||||
// ❌ User 가 임의 URL fetch
|
||||
app.post('/api/preview', async (req, res) => {
|
||||
const r = await fetch(req.body.url);
|
||||
res.json(await r.text());
|
||||
});
|
||||
// → http://169.254.169.254/ (AWS metadata).
|
||||
// → http://localhost/admin.
|
||||
|
||||
// ✅ Whitelist + DNS resolve check
|
||||
const url = new URL(req.body.url);
|
||||
if (!ALLOWED_HOSTS.includes(url.host)) return res.status(400).end();
|
||||
const ip = await dns.resolve(url.host);
|
||||
if (isPrivateIP(ip)) return res.status(400).end();
|
||||
|
||||
const r = await fetch(req.body.url);
|
||||
```
|
||||
|
||||
### API8: Security Misconfiguration
|
||||
```
|
||||
- Debug mode in production.
|
||||
- Default credentials.
|
||||
- CORS '*' (production).
|
||||
- TLS 약 (1.0, 1.1).
|
||||
- Verbose error message.
|
||||
- No security header.
|
||||
|
||||
→ Helmet (Node), Rails defaults, etc.
|
||||
```
|
||||
|
||||
```ts
|
||||
import helmet from 'helmet';
|
||||
app.use(helmet());
|
||||
```
|
||||
|
||||
### API9: Improper Inventory Management
|
||||
```
|
||||
- v1 API 가 deprecated 가 still up.
|
||||
- Test endpoint 가 production.
|
||||
- 매 endpoint 의 schema doc.
|
||||
|
||||
→ OpenAPI spec + audit.
|
||||
```
|
||||
|
||||
### API10: Unsafe Consumption of APIs
|
||||
```ts
|
||||
// ❌ External API 의 response 가 trust
|
||||
const user = await externalAPI.getUser(id);
|
||||
const html = `<div>Welcome ${user.name}</div>`; // XSS.
|
||||
|
||||
// ✅ Sanitize / validate
|
||||
const user = await externalAPI.getUser(id);
|
||||
const safe = sanitizeHTML(user.name);
|
||||
```
|
||||
|
||||
→ External API 도 input.
|
||||
|
||||
### Common patterns
|
||||
|
||||
#### JWT 함정
|
||||
```ts
|
||||
// ❌ Algorithm none
|
||||
jwt.verify(token, ''); // 어떤 token 도 valid.
|
||||
|
||||
// ✅
|
||||
jwt.verify(token, SECRET, { algorithms: ['RS256'] });
|
||||
```
|
||||
|
||||
#### Rate limit
|
||||
```ts
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
app.use('/api/', rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 100, // 100 req / min / IP
|
||||
}));
|
||||
|
||||
// Stricter for sensitive
|
||||
app.post('/api/login', rateLimit({ max: 5, windowMs: 15 * 60 * 1000 }), ...);
|
||||
```
|
||||
|
||||
#### Input validation (Zod)
|
||||
```ts
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
age: z.number().int().min(18),
|
||||
});
|
||||
|
||||
app.post('/api/users', (req, res) => {
|
||||
const result = schema.safeParse(req.body);
|
||||
if (!result.success) return res.status(400).json(result.error);
|
||||
|
||||
// result.data 가 typed + sanitized.
|
||||
});
|
||||
```
|
||||
|
||||
→ Strong typing + validation.
|
||||
|
||||
#### SQL injection (Prisma / Drizzle)
|
||||
```ts
|
||||
// ❌ String concat
|
||||
const r = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
|
||||
|
||||
// ✅ Parameterized
|
||||
const r = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
|
||||
|
||||
// Prisma 가 자동 safe
|
||||
const user = await prisma.user.findUnique({ where: { id: req.params.id } });
|
||||
```
|
||||
|
||||
#### CORS
|
||||
```ts
|
||||
import cors from 'cors';
|
||||
|
||||
app.use(cors({
|
||||
origin: ['https://app.example.com'],
|
||||
credentials: true,
|
||||
}));
|
||||
```
|
||||
|
||||
→ '*' 가 production X.
|
||||
|
||||
### Audit
|
||||
```bash
|
||||
# OWASP ZAP
|
||||
docker run owasp/zap2docker-stable zap-baseline.py -t https://api.example.com
|
||||
|
||||
# Burp Suite
|
||||
# Postman + automated test
|
||||
```
|
||||
|
||||
### Bug bounty
|
||||
```
|
||||
HackerOne, Bugcrowd.
|
||||
- Researcher 가 vulnerability 발견.
|
||||
- Reward 가 severity 따라.
|
||||
- Responsible disclosure.
|
||||
|
||||
→ Critical security fence.
|
||||
```
|
||||
|
||||
→ [[Security_Bug_Bounty]].
|
||||
|
||||
### Pen testing
|
||||
```
|
||||
Annual / quarterly:
|
||||
- 외부 firm 가 attack 시도.
|
||||
- Report.
|
||||
- Fix + retest.
|
||||
```
|
||||
|
||||
→ [[Security_Pen_Testing]].
|
||||
|
||||
### Monitoring
|
||||
```
|
||||
- Failed login (brute force).
|
||||
- Unusual request pattern.
|
||||
- 4xx / 5xx spike.
|
||||
- Geo anomaly.
|
||||
|
||||
→ Datadog / Sentry / SIEM.
|
||||
```
|
||||
|
||||
### Common 함정
|
||||
```
|
||||
- Test in production: skip security check.
|
||||
- "내부 API 만" 가정: not.
|
||||
- Auth 가 client 만: server 도.
|
||||
- Rate limit per IP only: per user 도.
|
||||
- CORS '*' + cookie: CSRF.
|
||||
- Verbose error: leak info.
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 위협 | 방어 |
|
||||
|---|---|
|
||||
| BOLA | Authorization check |
|
||||
| Mass assignment | Whitelist |
|
||||
| Brute force | Rate limit + bcrypt |
|
||||
| SSRF | URL whitelist + IP filter |
|
||||
| SQL injection | Parameterized / ORM |
|
||||
| XSS | Sanitize + output encode |
|
||||
| CSRF | Token + SameSite cookie |
|
||||
| Verbose error | Generic message |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No authorization (BOLA)**: data leak.
|
||||
- **Bcrypt 없음**: rainbow table.
|
||||
- **String concat SQL**: injection.
|
||||
- **CORS '*'**: CSRF.
|
||||
- **No rate limit**: brute force.
|
||||
- **Verbose error**: info leak.
|
||||
- **Production debug**: stack trace leak.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- BOLA 가 #1 흔한 bug.
|
||||
- Whitelist > blacklist.
|
||||
- Bcrypt + rate limit + JWT (algorithm).
|
||||
- ZAP / Burp 가 audit.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Security_OWASP_Top_10_Practical]]
|
||||
- [[Security_Auth_Authz_Patterns]]
|
||||
- [[Security_Input_Validation]]
|
||||
Reference in New Issue
Block a user