์น ์ ํ๋ฆฌ์ผ์ด์ ์ ํต์ฌ์ด API๋ก ์ด๋ํ๋ฉด์, ๋ณด์ ์ํ๋ ํจ๊ป ์ด๋ํ๋ค.
๋ชจ๋ฐ์ผ ์ฑ, SPA, ๋ง์ดํฌ๋ก์๋น์ค โ ๋ชจ๋ API๋ฅผ ํตํด ํต์ ํ๋ค.
๊ทธ๋ฐ๋ฐ ๋ง์ ๊ฐ๋ฐํ์ด ์น ๋ณด์์ ์ ๊ฒฝ ์ฐ๋ฉด์ API ๋ณด์์ ๋ณ๋๋ก ์๊ฐํ์ง ์๋๋ค.
OWASP๋ ์ด ๋ฌธ์ ๋ฅผ ์ธ์ํ๊ณ 2019๋
์ต์ด๋ก, 2023๋
๊ฐฑ์ ํ์ผ๋ก API Security Top 10์ ๋ฐํํ๋ค.
์น ์ ํ๋ฆฌ์ผ์ด์
Top 10๊ณผ๋ ๋ณ๋์ ๋ชฉ๋ก์ด๋ฉฐ, API ํน์ ์ ์ํ์ ๋ค๋ฃฌ๋ค.
์ด์ ๊ธ: ๐ OWASP Top 10:2025 ์์ ๊ฐ์ด๋ โ ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์
์ API ๋ณด์์ ๋ณ๋์ธ๊ฐ
์น ํ์ด์ง์ API์ ์ฐจ์ด:
| ๊ตฌ๋ถ | ์น ํ์ด์ง | API |
|---|---|---|
| ์ธํฐํ์ด์ค | HTML/CSS๋ก ํ์ ์ ์ด | ๋ฐ์ดํฐ๋ง ๋ฐํ (JSON, XML) |
| ์ ๊ทผ ๋ฐฉ์ | ๋ธ๋ผ์ฐ์ UI | ์ง์ HTTP ์์ฒญ |
| ์ธ์ฆ | ์ธ์ /์ฟ ํค ์ค์ฌ | ํ ํฐ ์ค์ฌ (JWT, API Key) |
| ๋ ธ์ถ ์์ค | ํ๋ฉด์ ๋ณด์ด๋ ๊ฒ๋ง | ์๋ต ๊ฐ์ฒด ์ ์ฒด |
| ๊ณต๊ฒฉ ํ๋ฉด | ์ ํ์ | ๋ชจ๋ ์๋ํฌ์ธํธ |
API๋ UI๊ฐ ์๊ธฐ ๋๋ฌธ์ ๋ธ๋ผ์ฐ์ ๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ๋ณดํธ๋ง๋ ์๋ค.
๊ณต๊ฒฉ์๋ Burp Suite๋ curl ํ ์ค๋ก ์ง์ API๋ฅผ ํธ์ถํ ์ ์๋ค.
2019 โ 2023, ๋ฌด์์ด ๋ฌ๋ผ์ก๋
ํต์ฌ ๋ณํ:
- "Excessive Data Exposure" + "Mass Assignment" โ BOPLA(API3)๋ก ํตํฉ
- SSRF(API7), ๋ฏผ๊ฐํ ๋น์ฆ๋์ค ํ๋ก์ฐ(API6), ์ธ๋ถ API ์ ๋ขฐ ๋ฌธ์ (API10) ์ ์ค
- Injection, Logging์ ์น Top 10์ผ๋ก ํตํฉ
API1:2023 โ Broken Object Level Authorization (BOLA)
๊ฐ์
๊ฐ์ฒด ๋ ๋ฒจ ์ ๊ทผ ์ ์ด ์คํจ. 2019์์๋, 2023์์๋ ๋ณํจ์์ด 1์๋ค.
ํํ IDOR(Insecure Direct Object Reference) ๋ผ๊ณ ๋ ๋ถ๋ฅธ๋ค.
API๋ ๋ฆฌ์์ค์ ์ง์ ID๋ฅผ ์ฌ์ฉํด ์ ๊ทผํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค.
์ด๋ "์ด ์ฌ์ฉ์๊ฐ ์ด ID์ ๋ฆฌ์์ค์ ์ ๊ทผํ ๊ถํ์ด ์๋๊ฐ" ๋ฅผ ๊ฒ์ฆํ์ง ์์ผ๋ฉด ๋์ฅ์ด๋ค.
์ทจ์ฝํ ํจํด
// ์ทจ์ฝํ ์ฝ๋: ์ธ์ฆ์ ํ์ง๋ง ์์ ๊ถ ๊ฒ์ฆ ์์
router.get('/api/orders/:orderId', authenticate, async (req, res) => {
// orderId๊ฐ 1, 2, 3, ... ์์ฐจ์ ์ด๋ฉด
// ๊ณต๊ฒฉ์๋ orderId๋ฅผ ๋ฐ๊ฟ๊ฐ๋ฉฐ ๋ค๋ฅธ ์ฌ๋ ์ฃผ๋ฌธ ์กฐํ ๊ฐ๋ฅ
const order = await Order.findById(req.params.orderId)
return res.json(order)
})
// ๊ณต๊ฒฉ์ ์์ฒญ:
// GET /api/orders/1001 โ ๋ด ์ฃผ๋ฌธ
// GET /api/orders/1002 โ ๋ค๋ฅธ ์ฌ๋ ์ฃผ๋ฌธ (์์ ๊ถ ๊ฒ์ฆ ์์)
// GET /api/orders/1003 โ ๋ ๋ค๋ฅธ ์ฌ๋ ์ฃผ๋ฌธ...// ์์ ํ ์ฝ๋: ์์ ๊ถ ๊ฒ์ฆ ํฌํจ
router.get('/api/orders/:orderId', authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.orderId,
userId: req.user.id // ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์์ ๊ฒ์ธ์ง ํ์ธ
})
if (!order) {
return res.status(404).json({ error: 'Not found' })
// 403์ด ์๋ 404 ๋ฐํ: ๋ฆฌ์์ค ์กด์ฌ ์ฌ๋ถ๋ ๋
ธ์ถํ์ง ์์
}
return res.json(order)
})๋ฐฉ์ด ์ ๋ต
- ๋ชจ๋ ๋ฐ์ดํฐ ์ ๊ทผ ์ ์์ ๊ถ ๊ฒ์ฆ (userId, orgId, tenantId ๋ฑ)
- ์์ฐจ์ ID ๋์ UUID ์ฌ์ฉ (์ถ์ธก์ ์ด๋ ต๊ฒ)
- ์๋ต์์ ์ต์ํ์ ๋ฐ์ดํฐ๋ง ๋ฐํ
- ์ ๊ทผ ์๋ ๋ก๊น โ ๋น์ ์ ํจํด ํ์ง
API2:2023 โ Broken Authentication
๊ฐ์
์ธ์ฆ ๋ฉ์ปค๋์ฆ์ ๊ฒฐํจ. API์์๋ ํนํ ํ ํฐ ๊ด๋ฆฌ ๋ฌธ์ ๊ฐ ํฌ๋ค.
์ทจ์ฝํ ํจํด๋ค
โ API Key ํ๋์ฝ๋ฉ
# ์ทจ์ฝํ ์ฝ๋: API ํค๊ฐ ์ฝ๋์ ๋ฐํ์์
headers = {
"Authorization": "Bearer sk-prod-abc123def456", # ์ค์ ํ๋ก๋์
ํค
"Content-Type": "application/json"
}
# GitHub, GitLab์ ์ด๋ฐ ์ฝ๋๊ฐ push๋๋ฉด ์์ด ๋ด๋ก ํค๊ฐ ํ์ทจ๋จ
# ์์ ํ ์ฝ๋
import os
headers = {
"Authorization": f"Bearer {os.environ.get('API_SECRET_KEY')}",
"Content-Type": "application/json"
}โก JWT ๊ฒ์ฆ ๋๋ฝ
// ์ทจ์ฝํ ์ฝ๋: JWT ์๋ช
๊ฒ์ฆ ์์ด ๋์ฝ๋ฉ๋ง
const jwt = require('jsonwebtoken')
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1]
// ์๋ช
๊ฒ์ฆ ์์ด ํ์ด๋ก๋๋ง ์ถ์ถ (๊ณต๊ฒฉ์๊ฐ ์์กฐ ๊ฐ๋ฅ)
const decoded = Buffer.from(token.split('.')[1], 'base64').toString()
req.user = JSON.parse(decoded)
next()
}
// ์์ ํ ์ฝ๋: ์๋ช
๊ฒ์ฆ ํ์
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1]
if (!token) return res.status(401).json({ error: 'No token' })
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'], // ํ์ฉ ์๊ณ ๋ฆฌ์ฆ ๋ช
์
issuer: 'my-api' // issuer ๊ฒ์ฆ
})
req.user = decoded
next()
} catch (err) {
return res.status(401).json({ error: 'Invalid token' })
}
}โข ํ ํฐ ๋ง๋ฃ ๋ฏธ์ฒ๋ฆฌ
// ์ทจ์ฝํ ์ฝ๋: ๋ง๋ฃ๋ ํ ํฐ ํ์ฉ
const decoded = jwt.verify(token, secret, {
ignoreExpiration: true // ์ ๋ ์ด๋ ๊ฒ ํ๋ฉด ์ ๋จ
})
// ์์ ํ ์ฝ๋: ์ ์ ํ ๋ง๋ฃ ์๊ฐ ์ค์
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m', issuer: 'my-api' } // 15๋ถ ๋ง๋ฃ
)
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' } // ๋ฆฌํ๋ ์๋ 7์ผ
)๋ฐฉ์ด ์ ๋ต
- API Key, ํ ํฐ์ ์ ๋ ์ฝ๋์ ํ๋์ฝ๋ฉ ๊ธ์ง (ํ๊ฒฝ๋ณ์, Secrets Manager)
- JWT: ์๋ช ๊ฒ์ฆ + ๋ง๋ฃ ๊ฒ์ฆ + ์๊ณ ๋ฆฌ์ฆ ๋ช ์
- ๋ฏผ๊ฐ API์๋ MFA ์ถ๊ฐ ๊ฒ์ฆ
- ํ ํฐ ๋ฌดํจํ ๋ฉ์ปค๋์ฆ ๊ตฌํ (๋ธ๋๋ฆฌ์คํธ ๋๋ short-lived token)
API3:2023 โ Broken Object Property Level Authorization (BOPLA)
๊ฐ์
2023 ์ ์ค. 2019์ "Excessive Data Exposure"์ "Mass Assignment"๋ฅผ ํตํฉํ ๊ฐ๋ ์ด๋ค.
๋ ๊ฐ์ง ๋ฌธ์ ๋ฅผ ํฉ์ณ์ ๋ค๋ฃฌ๋ค:
- ๊ณผ๋ํ ๋ฐ์ดํฐ ๋ ธ์ถ: API๊ฐ ํ์ ์ด์์ผ๋ก ๋ง์ ํ๋๋ฅผ ๋ฐํ
- ๋๋ ํ ๋น(Mass Assignment): ์ฌ์ฉ์๊ฐ ์์ ํ๋ฉด ์ ๋๋ ํ๋๊น์ง ์ ๋ฐ์ดํธ ํ์ฉ
๊ณผ๋ํ ๋ฐ์ดํฐ ๋ ธ์ถ
// ์ทจ์ฝํ ์ฝ๋: DB ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ๋ฐํ
router.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id)
return res.json(user)
// ๋ฐํ๋จ: { _id, name, email, passwordHash, role, ssn, creditCard, ... }
// ๋น๋ฐ๋ฒํธ ํด์, ๊ถํ, ์ฃผ๋ฏผ๋ฒํธ, ์นด๋๋ฒํธ๊น์ง ๋ค ๋๊ฐ
})
// ์์ ํ ์ฝ๋: ํ์ํ ํ๋๋ง ๋ช
์์ ์ ํ
router.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id)
.select('name email createdAt') // ํ์ํ ํ๋๋ง
return res.json(user)
})Mass Assignment
// ์ทจ์ฝํ ์ฝ๋: ์์ฒญ ๋ฐ๋๋ฅผ ๊ทธ๋๋ก DB์ ์ ์ฅ
router.put('/api/users/:id', authenticate, async (req, res) => {
// ๊ณต๊ฒฉ์๊ฐ body์ { "role": "admin" } ์ ์ถ๊ฐํ๋ฉด?
const user = await User.findByIdAndUpdate(req.params.id, req.body)
return res.json(user)
})
// ์์ ํ ์ฝ๋: ํ์ฉ ํ๋ ๋ช
์์ ์ถ์ถ
router.put('/api/users/:id', authenticate, async (req, res) => {
const { name, email, bio } = req.body // role, isAdmin ๋ฑ์ ์ ์ธ
const user = await User.findByIdAndUpdate(
req.params.id,
{ name, email, bio }, // ํ์ฉ๋ ํ๋๋ง
{ new: true, runValidators: true }
)
return res.json({ name: user.name, email: user.email })
})๋ฐฉ์ด ์ ๋ต
- ์๋ต DTO(Data Transfer Object) ํจํด ์ฌ์ฉ: ๋ฐํ ์คํค๋ง ๋ช ์
- ์ ๋ ฅ ๊ฒ์ฆ: allowlist ๋ฐฉ์์ผ๋ก ํ์ฉ ํ๋๋ง ์ฒ๋ฆฌ
- ORM์
select(),omit()์ ๊ทน ํ์ฉ - API ๋ช ์ธ(OpenAPI/Swagger)์ ์๋ต ์คํค๋ง ์ ์
API4:2023 โ Unrestricted Resource Consumption
๊ฐ์
๋ฆฌ์์ค ์๋น ์ ํ ์์. API์ Rate Limit์ด ์์ผ๋ฉด ๊ณต๊ฒฉ์๊ฐ ์๋ฒ๋ฅผ ๊ณผ๋ถํ์ํฌ ์ ์๋ค.
2019์ "Lack of Resources & Rate Limiting"์์ ๋ฒ์๊ฐ ํ์ฅ๋๋ค.
์ทจ์ฝํ ์๋๋ฆฌ์ค
ํ์ผ ์ ๋ก๋ ๊ณต๊ฒฉ:
# ๊ณต๊ฒฉ์๊ฐ ๋์ฉ๋ ํ์ผ์ ์ง์์ ์ผ๋ก ์
๋ก๋
while true; do
curl -X POST https://api.example.com/upload \
-F "file=@huge_file_10gb.bin"
done
# โ ๋์คํฌ ๊ณ ๊ฐ, ์ฒ๋ฆฌ ํ ๋ง๋น๋ฐฉ์ด ์ฝ๋
const rateLimit = require('express-rate-limit')
const slowDown = require('express-slow-down')
// API ์ ์ฒด Rate Limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // 100ํ ์ ํ
standardHeaders: true,
message: { error: 'Too many requests, please try again later.' }
})
// ํนํ ๋ฏผ๊ฐํ ์๋ํฌ์ธํธ (๋ก๊ทธ์ธ, ํ์๊ฐ์
)
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1์๊ฐ
max: 5, // 5ํ
skipSuccessfulRequests: true
})
// Slow Down: ์ ์ ์๋ต์ ๋๋ฆฌ๊ฒ
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50, // 50ํ ์ดํ
delayMs: hits => hits * 100 // ์ ์ง์ ์ง์ฐ
})
app.use('/api/', apiLimiter)
app.use('/api/auth/', authLimiter, speedLimiter)
// ํ์ผ ์
๋ก๋ ํฌ๊ธฐ ์ ํ
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 1
}
})๋ฐฉ์ด ์ ๋ต
- Rate Limiting: ๊ธ๋ก๋ฒ + ์๋ํฌ์ธํธ๋ณ + IP๋ณ
- ํ์ผ ํฌ๊ธฐ ์ ํ: ์ ๋ก๋ ์ฉ๋, ํ์ผ ํ์ ๊ฒ์ฆ
- ์์ฒญ ํ์์์: ๋๋ฌด ์ค๋ ๊ฑธ๋ฆฌ๋ ์์ฒญ์ ๊ฐ์ ์ข ๋ฃ
- ์ฟผ๋ฆฌ ๋ณต์ก๋ ์ ํ: GraphQL์ ๊ฒฝ์ฐ ๊น์ด/๋น์ฉ ์ ํ
API5:2023 โ Broken Function Level Authorization
๊ฐ์
๊ธฐ๋ฅ(ํจ์) ๋ ๋ฒจ ์ ๊ทผ ์ ์ด ์คํจ.
API1์ด "์ด ๋ฐ์ดํฐ์ ์ ๊ทผํ ์ ์๋๊ฐ"์๋ค๋ฉด, API5๋ "์ด ๊ธฐ๋ฅ์ ์คํํ ์ ์๋๊ฐ" ์ ๋ฌธ์ ๋ค.
์ผ๋ฐ ์ฌ์ฉ์๊ฐ ๊ด๋ฆฌ์ ์ ์ฉ ๊ธฐ๋ฅ์ ํธ์ถํ ์ ์๋ ๊ฒฝ์ฐ๋ค.
์ทจ์ฝํ ํจํด
// ์ทจ์ฝํ ์ฝ๋: HTTP ๋ฉ์๋๋ก๋ง ๊ตฌ๋ถ, ์ค์ ๊ถํ ๊ฒ์ฆ ์์
router.get('/api/users', authenticate, getAllUsers) // ์ผ๋ฐ ์ฌ์ฉ์์ฉ
router.delete('/api/users/:id', authenticate, deleteUser) // ๊ด๋ฆฌ์์ฉ์ธ๋ฐ ๊ถํ ํ์ธ ์์
// ๊ณต๊ฒฉ์: DELETE /api/users/123 โ ๋ค๋ฅธ ์ฌ์ฉ์ ๊ณ์ ์ญ์ ๊ฐ๋ฅ
// ์์ ํ ์ฝ๋
const requireAdmin = (req, res, next) => {
if (!req.user.roles?.includes('ADMIN')) {
return res.status(403).json({ error: 'Admin access required' })
}
next()
}
router.get('/api/users', authenticate, getAllUsers)
router.delete('/api/users/:id', authenticate, requireAdmin, deleteUser)
router.post('/api/admin/settings', authenticate, requireAdmin, updateSettings)์จ๊ฒจ์ง ๊ด๋ฆฌ์ ์๋ํฌ์ธํธ:
# ๊ณต๊ฒฉ์๊ฐ ์ถ์ธกํ๋ ๊ด๋ฆฌ์ ์๋ํฌ์ธํธ ํจํด
GET /api/v1/admin/users
GET /api/internal/users
POST /api/users/promote
DELETE /api/admin/delete-all
# ๋ชจ๋ ์ธ์ฆ+์ธ๊ฐ ๊ฒ์ฆ์ด ์์ผ๋ฉด ํธ์ถ ๊ฐ๋ฅ๋ฐฉ์ด ์ ๋ต
- ๊ธฐ๋ณธ๊ฐ์ ๊ฑฐ๋ถ: ๋ช ์์ ์ผ๋ก ํ์ฉํ์ง ์์ ๋ชจ๋ ๊ธฐ๋ฅ์ ์ฐจ๋จ
- ๊ด๋ฆฌ์ ๊ธฐ๋ฅ์ ๋ณ๋ ๊ถํ ๋ฏธ๋ค์จ์ด ์ ์ฉ
- API ๋ฌธ์ํ ์ ์ธ์ฆ ์๊ตฌ์ฌํญ ๋ช ์
- ์ ๊ธฐ์ ์ธ API ์๋ํฌ์ธํธ ์ธ๋ฒคํ ๋ฆฌ ๊ด๋ฆฌ
API6:2023 โ Unrestricted Access to Sensitive Business Flows
๊ฐ์
2023 ์ ์ค. ๊ธฐ์ ์ ์ทจ์ฝ์ ์ด ์๋ ๋น์ฆ๋์ค ๋ก์ง ์ ์ฉ์ ๋ค๋ฃฌ๋ค.
API ์์ฒด๋ ์ ์์ ์ผ๋ก ๋์ํ์ง๋ง, ์๋ํ๋ ๊ณต๊ฒฉ์ผ๋ก ๋น์ฆ๋์ค ํ๋ก์ธ์ค๋ฅผ ์ ์ฉํ๋ ๊ฒฝ์ฐ๋ค.
์ค์ ๊ณต๊ฒฉ ์๋๋ฆฌ์ค
ํ์ ์๋ ์ํ ์ ์ :
// ์ทจ์ฝํ ์ฝ๋: ๋ด์ด ์ด๋น ์๋ฐฑ ๋ฒ ์์ฒญํด ํ์ ์ํ ๋
์
router.post('/api/checkout', authenticate, async (req, res) => {
const { productId, quantity } = req.body
const product = await Product.findById(productId)
if (product.stock < quantity) {
return res.status(400).json({ error: 'Out of stock' })
}
// ์ฌ๊ณ ํ์ธ ํ ๊ตฌ๋งค ์ฒ๋ฆฌ
// ๋ด์ด ์๋ฐฑ ms ๊ฐ๊ฒฉ์ผ๋ก ์์ฒญํ๋ฉด ๋์์ ์ฌ๋ฌ ๋ฒ ๊ฒฐ์ ๊ฐ๋ฅ
await product.decrementStock(quantity)
return res.json({ success: true })
})
// ์์ ํ ์ฝ๋: ๋ถ์ฐ ๋ฝ(Distributed Lock) + Rate Limit + ๋ด ํ์ง
router.post('/api/checkout', authenticate, checkoutLimiter, async (req, res) => {
const lockKey = `checkout:${req.user.id}:${req.body.productId}`
const lock = await redisLock.acquire(lockKey, 5000) // 5์ด ๋ฝ
if (!lock) {
return res.status(429).json({ error: '์ด๋ฏธ ์ฒ๋ฆฌ ์ค์
๋๋ค.' })
}
try {
// ์์์ ์ฌ๊ณ ๊ฐ์ (race condition ๋ฐฉ์ง)
const result = await Product.findOneAndUpdate(
{ _id: req.body.productId, stock: { $gte: req.body.quantity } },
{ $inc: { stock: -req.body.quantity } },
{ new: true }
)
if (!result) return res.status(400).json({ error: '์ฌ๊ณ ๋ถ์กฑ' })
// ...๊ฒฐ์ ์ฒ๋ฆฌ
} finally {
await lock.release()
}
})๋ฐฉ์ด ์ ๋ต
- ๋ด ํ์ง: CAPTCHA, ํ๋ ๋ถ์, User-Agent ๊ฒ์ฆ
- ๋น์ฆ๋์ค ๋ ๋ฒจ Rate Limit: "์ฌ์ฉ์๋น ํ๋ฃจ 3ํ" ๊ฐ์ ๋ก์ง
- CAPTCHA: ๋ฏผ๊ฐํ ๋น์ฆ๋์ค ํ๋ก์ฐ ์ง์ ์ ์ ์ ์ฉ
- ์ด์ ํ๋ ํจํด ์ค์๊ฐ ๋ชจ๋ํฐ๋ง ๋ฐ ์๋ฆผ
API7:2023 โ Server Side Request Forgery (SSRF)
๊ฐ์
2023 ์ ์ค (์น Top 10:2021์ A10์ด์๋ค๊ฐ API ์ ์ฉ์ผ๋ก ์ด๋).
์๋ฒ๊ฐ ์ธ๋ถ URL ์์ฒญ์ ์ฒ๋ฆฌํ ๋ ๊ณต๊ฒฉ์๊ฐ ๊ทธ ์์ฒญ์ ์กฐ์ํ๋ ๊ณต๊ฒฉ์ด๋ค.
API์์ URL์ ๋ฐ์ ์ฒ๋ฆฌํ๋ ๊ธฐ๋ฅ (์นํ , ์ธ๋ค์ผ ์์ฑ, URL ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฑ)์ด ํนํ ์ํํ๋ค.
์ทจ์ฝํ ํจํด
import requests
# ์ทจ์ฝํ ์ฝ๋: ์ฌ์ฉ์๊ฐ ์ ๊ณตํ URL์ ๊ฒ์ฆ ์์ด ์์ฒญ
@app.route('/api/fetch-preview', methods=['POST'])
def fetch_preview():
url = request.json.get('url')
response = requests.get(url) # ๊ณต๊ฒฉ์๊ฐ url ์กฐ์ ๊ฐ๋ฅ
return jsonify({'content': response.text[:1000]})
# ๊ณต๊ฒฉ์ ์์ฒญ ์์:
# { "url": "http://169.254.169.254/latest/meta-data/" }
# โ AWS EC2 ๋ฉํ๋ฐ์ดํฐ ํ์ทจ (IAM ์๊ฒฉ์ฆ๋ช
, ์ธ์คํด์ค ์ ๋ณด)
# { "url": "http://internal-service.local/admin" }
# โ ๋ด๋ถ ์๋น์ค ์ ๊ทผ (๋ฐฉํ๋ฒฝ ์ฐํ)
# { "url": "file:///etc/passwd" }
# โ ๋ก์ปฌ ํ์ผ ์ฝ๊ธฐ (๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฐ๋ผ)# ์์ ํ ์ฝ๋
import ipaddress
from urllib.parse import urlparse
ALLOWED_SCHEMES = {'http', 'https'}
BLOCKED_HOSTS = {
'169.254.169.254', # AWS ๋ฉํ๋ฐ์ดํฐ
'169.254.170.2', # AWS ECS ๋ฉํ๋ฐ์ดํฐ
'metadata.google.internal', # GCP ๋ฉํ๋ฐ์ดํฐ
}
def is_safe_url(url):
try:
parsed = urlparse(url)
# ์คํด ๊ฒ์ฆ
if parsed.scheme not in ALLOWED_SCHEMES:
return False
# ์ฐจ๋จ๋ ํธ์คํธ ๊ฒ์ฆ
if parsed.hostname in BLOCKED_HOSTS:
return False
# Private IP ์ฐจ๋จ (๋ด๋ถ๋ง ์ ๊ทผ ๋ฐฉ์ง)
try:
ip = ipaddress.ip_address(parsed.hostname)
if ip.is_private or ip.is_loopback or ip.is_link_local:
return False
except ValueError:
pass # ๋๋ฉ์ธ๋ช
์ธ ๊ฒฝ์ฐ ํต๊ณผ
return True
except Exception:
return False
@app.route('/api/fetch-preview', methods=['POST'])
def fetch_preview():
url = request.json.get('url')
if not is_safe_url(url):
return jsonify({'error': 'ํ์ฉ๋์ง ์๋ URL์
๋๋ค.'}), 400
response = requests.get(url, timeout=5, allow_redirects=False)
return jsonify({'content': response.text[:1000]})๋ฐฉ์ด ์ ๋ต
- URL ํ์ฉ ๋ชฉ๋ก(allowlist): ์ ๋ขฐํ ์ ์๋ ๋๋ฉ์ธ๋ง ํ์ฉ
- Private IP, ๋ฃจํ๋ฐฑ, ๋งํฌ-๋ก์ปฌ ์ฃผ์ ์ฐจ๋จ
- ๋ฆฌ๋ค์ด๋ ํธ ๋นํ์ฉ:
allow_redirects=False - ํด๋ผ์ฐ๋ ํ๊ฒฝ: IMDSv2 ๊ฐ์ ์ ์ฉ (AWS EC2 ๋ฉํ๋ฐ์ดํฐ ๋ณดํธ)
- ์์๋ฐ์ด๋ ๋ฐฉํ๋ฒฝ์ผ๋ก ์๋ฒ์ ์ธ๋ถ ์์ฒญ ๋ฒ์ ์ ํ
API8:2023 โ Security Misconfiguration
๊ฐ์
API ํ๊ฒฝ ์ค์ ์ค๋ฅ. ์น Top 10:2025์ A02์ ๋์ผํ ๋ฒ์ฃผ์ง๋ง API ํนํ ๋ฌธ์ ๋ฅผ ๋ค๋ฃฌ๋ค.
API์์ ์์ฃผ ๋ฐ์ํ๋ ์ค์ ์ค๋ฅ
โ CORS ๊ณผ๋ํ ํ์ฉ
// ์ทจ์ฝํ ์ค์ : ๋ชจ๋ Origin ํ์ฉ
app.use(cors())
// ๋๋
app.use(cors({ origin: '*' }))
// ์ด๋ ๊ฒ ํ๋ฉด ์ด๋ค ๋๋ฉ์ธ์์๋ API ํธ์ถ ๊ฐ๋ฅ
// CSRF ๊ณต๊ฒฉ์ ๋
ธ์ถ๋ ์ ์์
// ์์ ํ ์ค์ : ํ์ฉ ๋๋ฉ์ธ ๋ช
์
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
].filter(Boolean)
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
},
credentials: true
}))โก ๋๋ฒ๊ทธ ๋ชจ๋ ํ๋ก๋์ ๋ ธ์ถ
# ์ทจ์ฝํ ์ค์
if __name__ == '__main__':
app.run(debug=True) # ํ๋ก๋์
์์๋ debug=True๋ฉด Werkzeug ๋๋ฒ๊ฑฐ ๋
ธ์ถ
# ์์ ํ ์ค์
import os
debug_mode = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
app.run(debug=debug_mode)โข HTTP ๋ณด์ ํค๋ ๋๋ฝ
// helmet.js๋ก ๊ฐํธํ๊ฒ ๋ณด์ ํค๋ ์ ์ฉ
const helmet = require('helmet')
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}))
// ์ปค์คํ
ํค๋ ์ถ๊ฐ
app.use((req, res, next) => {
res.setHeader('X-API-Version', '1.0')
res.removeHeader('X-Powered-By') // ์๋ฒ ๊ธฐ์ ์คํ ์จ๊ธฐ๊ธฐ
next()
})API9:2023 โ Improper Inventory Management
๊ฐ์
API ์ธ๋ฒคํ ๋ฆฌ ๊ด๋ฆฌ ์คํจ. "์์ ์ด ๋ณด์ ํ API๋ฅผ ํ์ ํ์ง ๋ชปํ๋" ๋ฌธ์ ๋ค.
๋ง์ดํฌ๋ก์๋น์ค, ๋ ๊ฑฐ์ ์์คํ , ๋ค์ํ API ๋ฒ์ ์ด ํผ์ฌํ๋ฉด ์ด๋ค API๊ฐ ์ด๋์ ์กด์ฌํ๋์ง์กฐ์ฐจ ๋ชจ๋ฅด๋ ์ํฉ์ด ์จ๋ค.
ํํ ์๋๋ฆฌ์ค
์ค์ ์ํ:
- ๊ตฌ๋ฒ์ API(
/api/v1/)๋ ๋ณด์ ํจ์น๊ฐ ์ ๋ ์ฑ๋ก ์ด์ - ๋ด๋ถ ํ ์คํธ API๊ฐ ํ๋ก๋์ ์ ๊ทธ๋๋ก ๋ ธ์ถ
- ์๋ํํฐ API ๋ฌธ์ํ ์์ด ์ด์ โ ๊ถํ ๋ฒ์ ๋ถ๋ช ํ
๋ฐฉ์ด ์ ๋ต
# OpenAPI/Swagger ๋ช
์ธ๋ก API ์ธ๋ฒคํ ๋ฆฌ ๊ด๋ฆฌ
openapi: "3.0.0"
info:
title: "My API"
version: "2.1.0"
x-api-status: "production" # ์ํ ๋ช
์
x-deprecated-versions:
- version: "1.0.0"
sunset-date: "2025-12-31"
paths:
/api/v2/users:
get:
summary: "์ฌ์ฉ์ ๋ชฉ๋ก ์กฐํ"
security:
- bearerAuth: []
# ...
/api/v1/users:
get:
deprecated: true # ๋ช
์์ deprecated ํ์
description: "v2๋ก ๋ง์ด๊ทธ๋ ์ด์
ํ์. 2025-12-31 ์ข
๋ฃ."- API Gateway ๋์ : ๋ชจ๋ API ํธ๋ํฝ ์ค์ํ
- OpenAPI ๋ช ์ธ ๊ธฐ๋ฐ ๋ฌธ์ํ + ๋ฒ์ ๊ด๋ฆฌ
- ์ฌ์ฉ๋์ง ์๋ API ์ ๊ธฐ์ ํ๊ธฐ (Sunset Policy)
- ์ธ๋ถ ๋ ธ์ถ API ๋ชฉ๋ก ์ฃผ๊ธฐ์ ๊ฐ์ฌ
API10:2023 โ Unsafe Consumption of APIs
๊ฐ์
2023 ์ ์ค. "์ธ๋ถ API๋ฅผ ์ ๋ขฐํ๋ ๊ฒ ์์ฒด๊ฐ ์ํ"์ด๋ผ๋ ๊ฐ๋ ์ด๋ค.
์์ ์ด ๋ง๋ API๋ฅผ ๋ณดํธํ๋ ๊ฒ์๋ง ์ง์คํ๊ณ , ์์ ์ด ํธ์ถํ๋ ์ธ๋ถ API๋ ์๋ฌต์ ์ผ๋ก ์ ๋ขฐํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค.
์ทจ์ฝํ ํจํด
// ์ทจ์ฝํ ์ฝ๋: ์ธ๋ถ API ์๋ต์ ๊ทธ๋๋ก ์ ๋ขฐ
async function getUserLocation(ipAddress) {
const response = await fetch(`https://third-party-geo.api/lookup/${ipAddress}`)
const data = await response.json()
// ์ธ๋ถ API๊ฐ ์
์์ ์ธ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๋ฉด?
// data.city = "<script>alert('xss')</script>"
return data // ๊ฒ์ฆ ์์ด ๋ฐํ โ XSS, Injection ์ํ
}
// ์ธ๋ถ API๊ฐ ์นจํด๋นํ ๊ฒฝ์ฐ (API3:2025 ๊ณต๊ธ๋ง ๊ณต๊ฒฉ)
// โ ์ฐ๋ฆฌ ์๋ฒ๊ฐ ์
์ฑ ์๋ต์ ์คํ// ์์ ํ ์ฝ๋: ์ธ๋ถ API ์๋ต๋ ๊ฒ์ฆ
const Joi = require('joi')
const geoResponseSchema = Joi.object({
ip: Joi.string().ip().required(),
city: Joi.string().max(100).required(),
country: Joi.string().length(2).required(),
latitude: Joi.number().min(-90).max(90).required(),
longitude: Joi.number().min(-180).max(180).required()
})
async function getUserLocation(ipAddress) {
const response = await fetch(`https://third-party-geo.api/lookup/${ipAddress}`, {
timeout: 3000 // ํ์์์ ์ค์
})
if (!response.ok) {
throw new Error(`External API error: ${response.status}`)
}
const data = await response.json()
// ์ธ๋ถ ์๋ต๋ ์คํค๋ง ๊ฒ์ฆ
const { error, value } = geoResponseSchema.validate(data)
if (error) {
throw new Error(`Invalid external API response: ${error.message}`)
}
return value // ๊ฒ์ฆ๋ ๋ฐ์ดํฐ๋ง ๋ฐํ
}๋ฐฉ์ด ์ ๋ต
- ์ธ๋ถ API ์๋ต๋ ์คํค๋ง ๊ฒ์ฆ (Joi, Zod, Pydantic ๋ฑ)
- ์ธ๋ถ API์ ํ์์์ ์ค์ (๋ฌดํ ๋๊ธฐ ๋ฐฉ์ง)
- HTTPS ๊ฐ์ + ์ธ์ฆ์ ๊ฒ์ฆ (
verify=True) - ์ธ๋ถ API ์ ๊ณต์ฌ์ ๋ณด์ ๊ณต์ง ๊ตฌ๋ (์นจํด ์ฌ๊ณ ์๋ฆผ)
- ์ธ๋ถ API๊ฐ ๋ค์ด๋์ ๋ Fallback ๋ก์ง ์ค๋น
์ ์ฒด ์ํ๋ ์์ฝ
| ํญ๋ชฉ | ์ด๋ฆ | ํต์ฌ ์ํ | ๋์ ๋์ด๋ |
|---|---|---|---|
| API1 | BOLA | ๋ค๋ฅธ ์ฌ์ฉ์ ๋ฆฌ์์ค ์ ๊ทผ | ์ค |
| API2 | Broken Auth | ํ ํฐ ํ์ทจ/์์กฐ | ์ค |
| API3 | BOPLA | ๋ฐ์ดํฐ ๊ณผ๋ค ๋ ธ์ถ/ํ๋ ์กฐ์ | ์ค |
| API4 | Resource Consumption | ์๋ฒ ๊ณผ๋ถํ/DDoS | ํ |
| API5 | BFLA | ๊ถํ ์๋ ๊ธฐ๋ฅ ์คํ | ์ค |
| API6 | Sensitive Business Flows | ๋น์ฆ๋์ค ๋ก์ง ์๋ํ ์ ์ฉ | ์ |
| API7 | SSRF | ๋ด๋ถ ์๋น์ค/๋ฉํ๋ฐ์ดํฐ ์ ๊ทผ | ์ |
| API8 | Misconfiguration | ์๋ชป๋ ์ค์ ์ผ๋ก ์ ์ฒด ๋ ธ์ถ | ํ |
| API9 | Inventory Management | ๋ฐฉ์น๋ API ํตํ ์นจํด | ์ค |
| API10 | Unsafe API Consumption | ์ธ๋ถ API ์๋ต ์ ๋ขฐ๋ก ์ธํ ํผํด | ์ค |
API ๋ณด์ ๊ฐ๋ฐ ์ฒดํฌ๋ฆฌ์คํธ
์ค๊ณ ๋จ๊ณ
- ๋ชจ๋ ์๋ํฌ์ธํธ์ ์ธ์ฆ/์ธ๊ฐ ์๊ตฌ์ฌํญ ๋ช ์
- ์๋ต ์คํค๋ง ์ ์ (๋ฐํํ ํ๋ ์ฌ์ ๋ช ํํ)
- Rate Limit ์ ์ฑ ์ค๊ณ (๊ธ๋ก๋ฒ / ์ฌ์ฉ์๋ณ / ์๋ํฌ์ธํธ๋ณ)
- ๋ฏผ๊ฐ ๋น์ฆ๋์ค ํ๋ก์ฐ ์๋ณ ๋ฐ ๋ณดํธ ๋ฐฉ์ ๊ณํ
๊ตฌํ ๋จ๊ณ
- ๋ชจ๋ ๋ฐ์ดํฐ ์ ๊ทผ์ ์์ ๊ถ ๊ฒ์ฆ (BOLA ๋ฐฉ์ง)
- ์์ฒญ ์ ๋ ฅ ์คํค๋ง ๊ฒ์ฆ (allowlist ๋ฐฉ์)
- ์๋ต DTO ์ฌ์ฉ (ํ์ํ ํ๋๋ง ๋ฐํ)
- Rate Limit ๋ฏธ๋ค์จ์ด ์ ์ฉ
- CORS ํ์ฉ ๋๋ฉ์ธ ๋ช ์์ ์ค์
- HTTP ๋ณด์ ํค๋ ์ค์
๋ฐฐํฌ ๋จ๊ณ
- API ์ธ๋ฒคํ ๋ฆฌ ๋ฌธ์ํ (OpenAPI/Swagger)
- ๊ตฌ๋ฒ์ API ํ๊ธฐ ๊ณํ ์๋ฆฝ
- ์๋ฌ ์๋ต์ ๋ด๋ถ ์ ๋ณด ๋ ธ์ถ ์์ ํ์ธ
- HTTPS ๊ฐ์ , ๋๋ฒ๊ทธ ๋ชจ๋ ๋นํ์ฑํ
- ์ธ๋ถ API ๋ชฉ๋ก ๋ฐ ์ ๋ขฐ ์์ค ์ ๋ฆฌ
์ด์ ๋จ๊ณ
- API ์์ฒญ ๋ก๊ทธ ์์ง ๋ฐ ์ด์ ํ์ง
- ๋น์ ์ ์ ๊ทผ ์๋ ์๋ฆผ ์ค์
- ์ธ๋ถ ์์กด์ฑ ๋ณด์ ๊ณต์ง ๊ตฌ๋
- ์ ๊ธฐ API ๊ฐ์ฌ (์ฌ์ฉํ์ง ์๋ ์๋ํฌ์ธํธ ํ๊ธฐ)
๋ง์น๋ฉฐ
OWASP API Security Top 10์ ๋ณด๋ฉด ๊ณตํต๋ ํจํด์ด ์๋ค.
"์๋ฒ๊ฐ ์ง์ ๋ง๋ ๊ฒ๋ง ์ ๋ขฐํ๋ผ"
- ์ฌ์ฉ์ ์ ๋ ฅ: ๊ฒ์ฆ
- ์ธ๋ถ API ์๋ต: ๊ฒ์ฆ
- JWT ํ ํฐ: ์๋ช ๊ฒ์ฆ
- ์์ฒญ URL: ๊ฒ์ฆ
- DB์์ ๊ฐ์ ธ์จ ๋ฐ์ดํฐ: ์์ ๊ถ ๊ฒ์ฆ
API ๋ณด์์ "ํ ๋ฒ ์ค์ ํ๋ฉด ๋"์ด ์๋๋ค.
์๋ก์ด ์๋ํฌ์ธํธ๊ฐ ์ถ๊ฐ๋ ๋๋ง๋ค, ์ธ๋ถ ์์กด์ฑ์ด ๋ฐ๋ ๋๋ง๋ค ๋ค์ ์ ๊ฒํด์ผ ํ๋ค.
๊ด๋ จ ๊ธ
- ๐ OWASP Top 10:2025 ์์ ๊ฐ์ด๋ โ ์น ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์์ ๊ธฐ์ด
- ๐ Spring Boot ์ธ์ฆ ๊ตฌํ ๊ฐ์ด๋ โ JWT, OAuth2 ์ค์ ๊ตฌํ
- ๐ OSI 7๊ณ์ธต, ์ธ์ฐ์ง ๋ง๊ณ ์ดํดํ์ โ ๋คํธ์ํฌ ๊ณ์ธต์์ API ํต์ ์ดํด
