๐Ÿ”Œ OWASP API Security Top 10:2023 ์™„์ „ ๊ฐ€์ด๋“œ: REST API ๋ณด์•ˆ ์ทจ์•ฝ์ ๊ณผ ๋ฐฉ์–ด ์ „๋žต

@leekh8 ยท April 27, 2026 ยท 16 min read

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ•ต์‹ฌ์ด 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, ๋ฌด์—‡์ด ๋‹ฌ๋ผ์กŒ๋‚˜

2019
2023
์ œ๊ฑฐ
API1 BOLA
API2 Broken Auth
API3 Broken Object Property Level Auth ๐Ÿ†•
API4 Unrestricted Resource Consumption
API5 Broken Function Level Auth
API6 Unrestricted Access to Sensitive Flows ๐Ÿ†•
API7 Server Side Request Forgery ๐Ÿ†•
API8 Security Misconfiguration
API9 Improper Inventory Management
API10 Unsafe Consumption of APIs ๐Ÿ†•
API1 Broken Object Level Auth
API2 Broken Auth
API3 Excessive Data Exposure
API4 Lack of Resources & Rate Limiting
API5 Broken Function Level Auth
API6 Mass Assignment
API7 Security Misconfiguration
API8 Injection
API9 Improper Assets Management
API10 Insufficient Logging

ํ•ต์‹ฌ ๋ณ€ํ™”:

  • "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"๋ฅผ ํ†ตํ•ฉํ•œ ๊ฐœ๋…์ด๋‹ค.

๋‘ ๊ฐ€์ง€ ๋ฌธ์ œ๋ฅผ ํ•ฉ์ณ์„œ ๋‹ค๋ฃฌ๋‹ค:

  1. ๊ณผ๋„ํ•œ ๋ฐ์ดํ„ฐ ๋…ธ์ถœ: API๊ฐ€ ํ•„์š” ์ด์ƒ์œผ๋กœ ๋งŽ์€ ํ•„๋“œ๋ฅผ ๋ฐ˜ํ™˜
  2. ๋Œ€๋Ÿ‰ ํ• ๋‹น(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"์—์„œ ๋ฒ”์œ„๊ฐ€ ํ™•์žฅ๋๋‹ค.

์ทจ์•ฝํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค

์ดˆ๋‹น 1000ํšŒ ์š”์ฒญ
๋ชจ๋“  ์š”์ฒญ ์ฒ˜๋ฆฌ
๊ณผ๋ถ€ํ•˜
๊ณต๊ฒฉ์ž
API ์„œ๋ฒ„
DB ์ฟผ๋ฆฌ ร— 1000
์„œ๋ฒ„ ๋‹ค์šด / ๋น„์šฉ ํญํƒ„

ํŒŒ์ผ ์—…๋กœ๋“œ ๊ณต๊ฒฉ:

# ๊ณต๊ฒฉ์ž๊ฐ€ ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ์„ ์ง€์†์ ์œผ๋กœ ์—…๋กœ๋“œ
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 ์ž์ฒด๋Š” ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜์ง€๋งŒ, ์ž๋™ํ™”๋œ ๊ณต๊ฒฉ์œผ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค๋ฅผ ์•…์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋‹ค.

์‹ค์ œ ๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค

1ํšŒ
์ดˆ๋‹น 100ํšŒ
์ •์ƒ ์‚ฌ์šฉ์ž
๊ณต๊ฒฉ์ž
์ •์ƒ ์‚ฌ์šฉ์ž

์ฟ ํฐ 1๊ฐœ ์‚ฌ์šฉ
์ฟ ํฐ API
๊ณต๊ฒฉ์ž ๋ด‡

์Šคํฌ๋ฆฝํŠธ ์ž๋™ํ™”
ํ• ์ธ ์ฝ”๋“œ ์ƒ์„ฑ
1๊ฐœ ํ• ์ธ ์ฝ”๋“œ
์ˆ˜์ฒœ ๊ฐœ ํ• ์ธ ์ฝ”๋“œ

๋ฌดํ•œ ํ˜œํƒ ํƒˆ์ทจ

ํ•œ์ • ์ˆ˜๋Ÿ‰ ์ƒํ’ˆ ์„ ์ :

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ๋ด‡์ด ์ดˆ๋‹น ์ˆ˜๋ฐฑ ๋ฒˆ ์š”์ฒญํ•ด ํ•œ์ • ์ƒํ’ˆ ๋…์ 
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 v1 (๋ณด์•ˆ ํŒจ์น˜ ์™„๋ฃŒ)
API v2 (ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘)
API v1-beta (๊ฐœ๋ฐœ ์ค‘ ์žŠํ˜€์ง)
API-internal (ํ…Œ์ŠคํŠธ์šฉ, ๋ฐฉ์น˜)
๊ณต๊ฒฉ์ž
์ทจ์•ฝ์  ์•…์šฉ

์‹ค์ œ ์œ„ํ—˜:

  • ๊ตฌ๋ฒ„์ „ 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 ๋ณด์•ˆ์€ "ํ•œ ๋ฒˆ ์„ค์ •ํ•˜๋ฉด ๋"์ด ์•„๋‹ˆ๋‹ค.
์ƒˆ๋กœ์šด ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค, ์™ธ๋ถ€ ์˜์กด์„ฑ์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ๋‹ค์‹œ ์ ๊ฒ€ํ•ด์•ผ ํ•œ๋‹ค.


๊ด€๋ จ ๊ธ€


@leekh8
๋ณด์•ˆ, ์›น ๊ฐœ๋ฐœ, Python์„ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ