OWASP API Security Top 10(2023)

OWASP API Security Top 10 2023. ์›น์šฉ Top 10๊ณผ๋Š” ๋‹ค๋ฅธ, API์— ํŠนํ™”๋œ ๋ณด์•ˆ ์œ„ํ˜‘ ํ‘œ์ค€. ์ƒ์œ„๊ถŒ์„ ์ ๋ นํ•œ ์ธ๊ฐ€(Authorization) ๋ฌธ์ œ๋ถ€ํ„ฐ ๋น„์ฆˆ๋‹ˆ์Šค ํ๋ฆ„ ์•…์šฉ๊นŒ์ง€, ๋ฐฑ์—”๋“œ API ๊ฐœ๋ฐœ์ž๊ฐ€ ์•Œ์•„์•ผ ํ•  10๊ฐ€์ง€๋ฅผ ๊ณต๊ฒฉ ์˜ˆ์‹œ์™€ ๋Œ€์‘ ์ฝ”๋“œ๋กœ ์ •๋ฆฌํ•œ๋‹ค.


๋“ค์–ด๊ฐ€๋ฉฐ

์ง€๋‚œ ๊ธ€์—์„œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์šฉ OWASP Top 10:2025๋ฅผ ๋‹ค๋ค˜๋‹ค. ๊ทธ๋Ÿฐ๋ฐ OWASP์—๋Š” API์— ํŠนํ™”๋œ ๋ณ„๋„์˜ Top 10์ด ์žˆ๋‹ค. ๋ฐ”๋กœ OWASP API Security Top 10์ด๋‹ค.

๋‘ ๋ฌธ์„œ๋Š” ๋‹ค๋ฅด๋‹ค.
OWASP Top 10 (์›น): ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „๋ฐ˜์˜ ์œ„ํ˜‘. 2017 → 2021 → 2025.
OWASP API Security Top 10 (API): REST·GraphQL ๊ฐ™์€ API์— ํŠนํ™”๋œ ์œ„ํ˜‘. 2019 → 2023.
์ด๋ฆ„์ด ๋น„์Šทํ•ด ํ—ท๊ฐˆ๋ฆฌ์ง€๋งŒ, ๋Œ€์ƒ๊ณผ ํ•ญ๋ชฉ์ด ๋‹ค๋ฅธ ๋ณ„๊ฐœ์˜ ๋ฌธ์„œ๋‹ค. ์ด ๊ธ€์€ API ๋ฒ„์ „(2023)์„ ๋‹ค๋ฃฌ๋‹ค.

์š”์ฆ˜์ฒ˜๋Ÿผ ํ”„๋ก ํŠธ์™€ ๋ฐฑ์—”๋“œ๊ฐ€ ๋ถ„๋ฆฌ๋˜๊ณ , ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์™€ ๋ชจ๋ฐ”์ผ ์•ฑ์ด API๋กœ ํ†ต์‹ ํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” API ๋ณด์•ˆ์ด ๊ณง ์„œ๋น„์Šค ๋ณด์•ˆ์ด๋‹ค. ํฅ๋ฏธ๋กœ์šด ์ ์€, API ๋ณด์•ˆ์˜ ํ•ต์‹ฌ์ด ๋Œ€๋ถ€๋ถ„ ์ธ๊ฐ€(Authorization) ์— ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์ƒ์œ„ 5๊ฐœ ์ค‘ 3๊ฐœ๊ฐ€ ์ธ๊ฐ€ ๊ด€๋ จ ํ•ญ๋ชฉ์ด๋‹ค.

์˜ˆ์‹œ๋Š” Java/Spring ๊ธฐ์ค€์ด๋‹ค.


2019 → 2023, ๋ฌด์—‡์ด ๋ฐ”๋€Œ์—ˆ๋‚˜

API ๋ฒ„์ „์€ 2019๋…„ ์ฒซ ๋ฐœ๊ฐ„ ํ›„ 4๋…„ ๋งŒ์ธ 2023๋…„์— ๊ฐœ์ •๋๋‹ค. ์ฃผ์š” ๋ณ€ํ™”๋Š” ์ด๋ ‡๋‹ค.

  • ์‹ ๊ทœ: API6(๋ฏผ๊ฐํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ๋ฆ„ ๋ฌด์ œํ•œ ์ ‘๊ทผ), API7(SSRF), API10(์•ˆ์ „ํ•˜์ง€ ์•Š์€ API ์†Œ๋น„)
  • ํ†ตํ•ฉ: ๊ธฐ์กด "๊ณผ๋„ํ•œ ๋ฐ์ดํ„ฐ ๋…ธ์ถœ"๊ณผ "Mass Assignment"๊ฐ€ API3(๊ฐ์ฒด ์†์„ฑ ์ˆ˜์ค€ ์ธ๊ฐ€)๋กœ ํ•ฉ์ณ์ง
  • ํ™•์žฅ: "๋ฆฌ์†Œ์Šค ๋ถ€์กฑ·์†๋„ ์ œํ•œ"์ด API4(๋ฌด์ œํ•œ ๋ฆฌ์†Œ์Šค ์†Œ๋น„)๋กœ ๋ฒ”์œ„ ํ™•๋Œ€

์ „์ฒด ๋ชฉ๋ก์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์ˆœ์œ„ ํ•ญ๋ชฉ ๋ถ„๋ฅ˜
API1 Broken Object Level Authorization (BOLA) ์ธ๊ฐ€
API2 Broken Authentication ์ธ์ฆ
API3 Broken Object Property Level Authorization ์ธ๊ฐ€
API4 Unrestricted Resource Consumption ์ž์›
API5 Broken Function Level Authorization (BFLA) ์ธ๊ฐ€
API6 Unrestricted Access to Sensitive Business Flows ์„ค๊ณ„
API7 Server Side Request Forgery (SSRF) ์ž…๋ ฅ
API8 Security Misconfiguration ์„ค์ •
API9 Improper Inventory Management ๊ด€๋ฆฌ
API10 Unsafe Consumption of APIs ์™ธ๋ถ€์—ฐ๋™

API1. Broken Object Level Authorization (BOLA) — ๊ฐ์ฒด ์ˆ˜์ค€ ์ธ๊ฐ€ ์‹คํŒจ

API ๋ณด์•ˆ์˜ ๋ถ€๋™์˜ 1์œ„. ์š”์ฒญํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์ง€ ์•Š์„ ๋•Œ ๋ฐœ์ƒํ•œ๋‹ค. ์›น Top 10์˜ IDOR์™€ ๊ฐ™์€ ๋ฟŒ๋ฆฌ๋‹ค.

// ์ทจ์•ฝ: id๋กœ ์กฐํšŒ๋งŒ ํ•˜๊ณ  ์†Œ์œ ์ž ํ™•์ธ ์—†์Œ
@GetMapping("/api/accounts/{accountId}")
public Account getAccount(@PathVariable Long accountId) {
    return accountRepository.findById(accountId).orElseThrow();
    // accountId๋ฅผ 1์”ฉ ๋ฐ”๊พธ๋ฉด ๋‚จ์˜ ๊ณ„์ขŒ๊ฐ€ ๊ทธ๋Œ€๋กœ ์กฐํšŒ๋œ๋‹ค
}
// ๋Œ€์‘: ์š”์ฒญ์ž๊ฐ€ ํ•ด๋‹น ๊ฐ์ฒด์˜ ์†Œ์œ ์ž์ธ์ง€ ํ•ญ์ƒ ๊ฒ€์ฆ
@GetMapping("/api/accounts/{accountId}")
public Account getAccount(@PathVariable Long accountId,
                          @AuthenticationPrincipal UserDetails user) {
    Account account = accountRepository.findById(accountId).orElseThrow();
    if (!account.getOwnerId().equals(user.getId())) {
        throw new AccessDeniedException("์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค");
    }
    return account;
}

๋Œ€์‘ ์š”์•ฝ: ๋ชจ๋“  ๊ฐ์ฒด ์ ‘๊ทผ์— ์†Œ์œ ๊ถŒ์„ ๊ฒ€์ฆํ•œ๋‹ค.
์ˆœ์ฐจ์  ์ •์ˆ˜ ID ๋Œ€์‹  ์ถ”์ธกํ•˜๊ธฐ ์–ด๋ ค์šด UUID๋ฅผ ์“ฐ๋Š” ๊ฒƒ๋„ ๋ฐฉ์–ด์— ๋„์›€์ด ๋œ๋‹ค(๊ทผ๋ณธ ํ•ด๊ฒฐ์€ ์•„๋‹ˆ์ง€๋งŒ).


API2. Broken Authentication — ์ธ์ฆ ์‹คํŒจ

์ธ์ฆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์ž˜๋ชป ๊ตฌํ˜„๋œ ๊ฒฝ์šฐ๋‹ค. ํ† ํฐ ๊ฒ€์ฆ ๋ˆ„๋ฝ, ์•ฝํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ—ˆ์šฉ, JWT ๋งŒ๋ฃŒ ๋ฏธ๊ฒ€์ฆ ๋“ฑ์ด ํฌํ•จ๋œ๋‹ค.

// ์ทจ์•ฝ: JWT ์„œ๋ช…๋งŒ ๋ณด๊ณ  ๋งŒ๋ฃŒ(exp)๋ฅผ ๊ฒ€์ฆํ•˜์ง€ ์•Š์Œ
Claims claims = Jwts.parser().verifyWith(key).build()
        .parseSignedClaims(token).getPayload();
// ๋งŒ๋ฃŒ๋œ ํ† ํฐ๋„ ํ†ต๊ณผ → ํƒˆ์ทจ๋œ ํ† ํฐ์ด ์˜์›ํžˆ ์œ ํšจ
// ๋Œ€์‘: ๋งŒ๋ฃŒ ๊ฒ€์ฆ์€ jjwt๊ฐ€ ์ž๋™์œผ๋กœ ํ•˜์ง€๋งŒ, ์˜ˆ์™ธ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ฒ˜๋ฆฌ
try {
    Claims claims = Jwts.parser().verifyWith(key).build()
            .parseSignedClaims(token).getPayload();
} catch (ExpiredJwtException e) {
    throw new UnauthorizedException("ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค");
} catch (JwtException e) {
    throw new UnauthorizedException("์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค");
}

๋Œ€์‘ ์š”์•ฝ: ๊ฒ€์ฆ๋œ ์ธ์ฆ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์“ฐ๊ณ , ํ† ํฐ ๋งŒ๋ฃŒ·์„œ๋ช…์„ ๋น ์ง์—†์ด ๊ฒ€์ฆํ•œ๋‹ค.
๋กœ๊ทธ์ธ ๋ฌด์ฐจ๋ณ„ ๋Œ€์ž…์— ์†๋„ ์ œํ•œ์„ ๊ฑธ๊ณ , ๋ฏผ๊ฐ ์ •๋ณด ๋ณ€๊ฒฝ ์‹œ ์žฌ์ธ์ฆ์„ ์š”๊ตฌํ•œ๋‹ค. (์ด์ „ JWT ์ธ์ฆ ๊ธ€ ์ฐธ๊ณ )


API3. Broken Object Property Level Authorization — ๊ฐ์ฒด ์†์„ฑ ์ˆ˜์ค€ ์ธ๊ฐ€ ์‹คํŒจ

๊ฐ์ฒด ์ ‘๊ทผ์€ ํ—ˆ์šฉ๋˜์ง€๋งŒ, ๊ทธ ๊ฐ์ฒด์˜ ํŠน์ • ์†์„ฑ๊นŒ์ง€๋Š” ํ†ต์ œํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋‹ค.
2019๋…„์˜ "๊ณผ๋„ํ•œ ๋ฐ์ดํ„ฐ ๋…ธ์ถœ"๊ณผ "Mass Assignment"๊ฐ€ ์—ฌ๊ธฐ๋กœ ํ•ฉ์ณ์กŒ๋‹ค.

// ์ทจ์•ฝ: ์š”์ฒญ ๋ณธ๋ฌธ์„ ์—”ํ‹ฐํ‹ฐ์— ๊ทธ๋Œ€๋กœ ๋ฐ”์ธ๋”ฉ (Mass Assignment)
@PatchMapping("/api/users/{id}")
public User update(@PathVariable Long id, @RequestBody User body) {
    User user = userRepository.findById(id).orElseThrow();
    user.setName(body.getName());
    user.setRole(body.getRole());   // ์‚ฌ์šฉ์ž๊ฐ€ role=ADMIN ์„ ๋ณด๋‚ด๋ฉด ๊ถŒํ•œ ์ƒ์Šน!
    return userRepository.save(user);
}
// ๋Œ€์‘: ์ˆ˜์ • ๊ฐ€๋Šฅํ•œ ์†์„ฑ๋งŒ ๋ฐ›๋Š” ์ „์šฉ DTO ์‚ฌ์šฉ
public record UpdateUserRequest(String name) {}   // role์€ ์•„์˜ˆ ๋ฐ›์ง€ ์•Š์Œ

@PatchMapping("/api/users/{id}")
public User update(@PathVariable Long id, @RequestBody UpdateUserRequest req) {
    User user = userRepository.findById(id).orElseThrow();
    user.setName(req.name());   // ํ—ˆ์šฉ๋œ ํ•„๋“œ๋งŒ ๋ณ€๊ฒฝ
    return userRepository.save(user);
}

๋Œ€์‘ ์š”์•ฝ: ์‘๋‹ต์€ ํ•„์š”ํ•œ ์†์„ฑ๋งŒ ๋‹ด์€ DTO๋กœ ๋‚ด๋ณด๋‚ด๊ณ (๊ณผ๋„ํ•œ ๋…ธ์ถœ ๋ฐฉ์ง€),
์š”์ฒญ๋„ ์ˆ˜์ • ํ—ˆ์šฉ ์†์„ฑ๋งŒ ๋ฐ›๋Š” DTO๋กœ ๋ฐ›๋Š”๋‹ค(Mass Assignment ๋ฐฉ์ง€). ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์ž…์ถœ๋ ฅ์— ๋…ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค.


API4. Unrestricted Resource Consumption — ๋ฌด์ œํ•œ ๋ฆฌ์†Œ์Šค ์†Œ๋น„

์†๋„ ์ œํ•œ์ด ์—†์–ด API๊ฐ€ ์ž์›์„ ๋ฌด์ œํ•œ์œผ๋กœ ์†Œ๋น„ํ•˜๋Š” ๊ฒฝ์šฐ๋‹ค. DoS ๊ณต๊ฒฉ์ด๋‚˜ ๊ณผ๊ธˆ ํญํƒ„์œผ๋กœ ์ด์–ด์ง„๋‹ค.

// ๋Œ€์‘: Resilience4j ๋“ฑ์œผ๋กœ ์†๋„ ์ œํ•œ
@RateLimiter(name = "api", fallbackMethod = "fallback")
@GetMapping("/api/search")
public List<Result> search(@RequestParam String query,
                           @RequestParam(defaultValue = "20") int size) {
    int limited = Math.min(size, 100);   // ํŽ˜์ด์ง€ ํฌ๊ธฐ ์ƒํ•œ๋„ ๊ฐ•์ œ
    return searchService.search(query, limited);
}

๋Œ€์‘ ์š”์•ฝ: ์š”์ฒญ ํšŸ์ˆ˜·ํŽ˜์ด๋กœ๋“œ ํฌ๊ธฐ·ํŽ˜์ด์ง€ ํฌ๊ธฐ·์‹คํ–‰ ์‹œ๊ฐ„์— ์ƒํ•œ์„ ๋‘”๋‹ค.
๋ฌด๊ฑฐ์šด ์—ฐ์‚ฐ(ํŒŒ์ผ ์—…๋กœ๋“œ, ์™ธ๋ถ€ ํ˜ธ์ถœ)์—๋Š” ํƒ€์ž„์•„์›ƒ๊ณผ ์ฟผํ„ฐ๋ฅผ ์ ์šฉํ•œ๋‹ค.


API5. Broken Function Level Authorization (BFLA) — ํ•จ์ˆ˜ ์ˆ˜์ค€ ์ธ๊ฐ€ ์‹คํŒจ

๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ ๊ธฐ๋Šฅ(์—”๋“œํฌ์ธํŠธ) ์ž์ฒด์— ๋Œ€ํ•œ ๊ถŒํ•œ ๊ฒ€์ฆ์ด ๋น ์ง„ ๊ฒฝ์šฐ๋‹ค. ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๊ฐ€ ๊ด€๋ฆฌ์ž API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ด๋‹ค.

// ์ทจ์•ฝ: ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ์— ๊ถŒํ•œ ๊ฒ€์‚ฌ ์—†์Œ
@DeleteMapping("/api/admin/users/{id}")
public void deleteUser(@PathVariable Long id) {
    userRepository.deleteById(id);   // ๋ˆ„๊ตฌ๋‚˜ ํ˜ธ์ถœ ๊ฐ€๋Šฅ
}
// ๋Œ€์‘: ๋ฉ”์„œ๋“œ ์ˆ˜์ค€ ๊ถŒํ•œ ๊ฒ€์‚ฌ
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/api/admin/users/{id}")
public void deleteUser(@PathVariable Long id) {
    userRepository.deleteById(id);
}

๋Œ€์‘ ์š”์•ฝ: ๊ด€๋ฆฌ์ž/์ผ๋ฐ˜ ๊ธฐ๋Šฅ์„ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•˜๊ณ , ์—”๋“œํฌ์ธํŠธ๋งˆ๋‹ค ์—ญํ•  ๊ธฐ๋ฐ˜ ๊ถŒํ•œ์„ ๊ฒ€์ฆํ•œ๋‹ค.
๊ธฐ๋ณธ์€ ๊ฑฐ๋ถ€(deny by default), ๊ถŒํ•œ์ด ์žˆ๋Š” ์—ญํ• ๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ํ—ˆ์šฉํ•œ๋‹ค.


API6. Unrestricted Access to Sensitive Business Flows — ๋ฏผ๊ฐํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ๋ฆ„ ๋ฌด์ œํ•œ ์ ‘๊ทผ (์‹ ๊ทœ)

๊ธฐ์ˆ ์  ์ทจ์•ฝ์ ์€ ์—†์ง€๋งŒ, ๋น„์ฆˆ๋‹ˆ์Šค ํ๋ฆ„์ด ์ž๋™ํ™”๋กœ ์•…์šฉ๋˜๋Š” ๊ฒฝ์šฐ๋‹ค.
ํ•œ์ •ํŒ ์ƒํ’ˆ์„ ๋ด‡์ด ์‹น์“ธ์ดํ•˜๊ฑฐ๋‚˜, ์˜ˆ์•ฝ์„ ๋Œ€๋Ÿ‰ ์„ ์ ํ•˜๋Š” ์‹์ด๋‹ค.

// ๋Œ€์‘ ์˜ˆ: ๋ฏผ๊ฐ ํ๋ฆ„์— ๋ด‡ ๋ฐฉ์–ด + ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ์ œํ•œ
@PostMapping("/api/orders/limited-edition")
public Order purchase(@AuthenticationPrincipal UserDetails user,
                      @RequestBody OrderRequest req) {
    if (!captchaService.verify(req.captchaToken())) {   // ๋ด‡ ์ฐจ๋‹จ
        throw new BadRequestException("์ธ์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค");
    }
    if (orderService.countToday(user.getId()) >= 2) {   // 1์ธ๋‹น ์ˆ˜๋Ÿ‰ ์ œํ•œ
        throw new BusinessException("๊ตฌ๋งค ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค");
    }
    return orderService.create(user, req);
}

๋Œ€์‘ ์š”์•ฝ: ํ๋ฆ„์ด ์•…์šฉ๋  ๊ฐ€๋Šฅ์„ฑ์„ ์„ค๊ณ„ ๋‹จ๊ณ„์—์„œ ์‹๋ณ„ํ•œ๋‹ค.
๋ด‡ ํƒ์ง€(CAPTCHA, ๋””๋ฐ”์ด์Šค ํ•‘๊ฑฐํ”„๋ฆฐํŒ…), 1์ธ๋‹น ์ˆ˜๋Ÿ‰·๋นˆ๋„ ์ œํ•œ, ๋น„์ •์ƒ ํŒจํ„ด ํƒ์ง€๋ฅผ ์ ์šฉํ•œ๋‹ค.


API7. Server Side Request Forgery (SSRF) — ์„œ๋ฒ„ ์ธก ์š”์ฒญ ์œ„์กฐ (์‹ ๊ทœ)

API๊ฐ€ ์‚ฌ์šฉ์ž๊ฐ€ ์ค€ URL๋กœ ์„œ๋ฒ„๊ฐ€ ์š”์ฒญ์„ ๋ณด๋‚ด๊ฒŒ ๋งŒ๋“œ๋Š” ๊ณต๊ฒฉ์ด๋‹ค.
๋‚ด๋ถ€๋ง์ด๋‚˜ ํด๋ผ์šฐ๋“œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ฃผ์†Œ์— ์ ‘๊ทผ๋‹นํ•  ์ˆ˜ ์žˆ๋‹ค. (์›น 2025ํŒ์—์„œ๋Š” ์ ‘๊ทผ ์ œ์–ด๋กœ ํก์ˆ˜๋์ง€๋งŒ, API์—์„œ๋Š” ๋ณ„๋„ ํ•ญ๋ชฉ์ด๋‹ค.)

// ์ทจ์•ฝ: ์‚ฌ์šฉ์ž๊ฐ€ ์ค€ URL์„ ๊ทธ๋Œ€๋กœ ํ˜ธ์ถœ
@PostMapping("/api/fetch-image")
public byte[] fetchImage(@RequestBody String imageUrl) {
    return restClient.get().uri(imageUrl).retrieve().body(byte[].class);
    // imageUrl=http://169.254.169.254/... → ํด๋ผ์šฐ๋“œ ์ž๊ฒฉ์ฆ๋ช… ํƒˆ์ทจ
}
// ๋Œ€์‘: ํ—ˆ์šฉ๋œ ๋„๋ฉ”์ธ๋งŒ ํ†ต๊ณผ + ๋‚ด๋ถ€ IP ์ฐจ๋‹จ
private static final Set<String> ALLOWED = Set.of("cdn.example.com");

@PostMapping("/api/fetch-image")
public byte[] fetchImage(@RequestBody String imageUrl) {
    URI uri = URI.create(imageUrl);
    if (!ALLOWED.contains(uri.getHost()) || isInternalAddress(uri.getHost())) {
        throw new BadRequestException("ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค");
    }
    return restClient.get().uri(uri).retrieve().body(byte[].class);
}

๋Œ€์‘ ์š”์•ฝ: ์™ธ๋ถ€ URL์€ ํ—ˆ์šฉ ๋ชฉ๋ก(allowlist)์œผ๋กœ๋งŒ ๋ฐ›๊ณ , ๋‚ด๋ถ€๋ง·๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ฃผ์†Œ(169.254.169.254, localhost, ์‚ฌ์„ค IP ๋Œ€์—ญ)๋ฅผ ์ฐจ๋‹จํ•œ๋‹ค. ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋„ ๋”ฐ๋ผ๊ฐ€์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.


API8. Security Misconfiguration — ๋ณด์•ˆ ์„ค์ • ์˜ค๋ฅ˜

๋ถˆํ•„์š”ํ•˜๊ฒŒ ์—ด๋ฆฐ ๋ฉ”์„œ๋“œ, ๋ˆ„๋ฝ๋œ ๋ณด์•ˆ ํ—ค๋”, ๊ณผ๋„ํ•œ CORS ํ—ˆ์šฉ, ์ƒ์„ธ ์—๋Ÿฌ ๋…ธ์ถœ ๋“ฑ. ์›น ๋ฒ„์ „๊ณผ ๊ฐ™์€ ๋งฅ๋ฝ์ด์ง€๋งŒ API์—์„œ ํŠนํžˆ CORS๊ฐ€ ์ž์ฃผ ๋ฌธ์ œ๋‹ค.

// ์ทจ์•ฝ: ๋ชจ๋“  ์ถœ์ฒ˜ ํ—ˆ์šฉ
config.setAllowedOrigins(List.of("*"));
config.setAllowCredentials(true);   // ์™€์ผ๋“œ์นด๋“œ + ์ธ์ฆ์ •๋ณด = ์œ„ํ—˜
// ๋Œ€์‘: ์‹ ๋ขฐํ•˜๋Š” ์ถœ์ฒ˜๋งŒ ๋ช…์‹œ
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST"));
config.setAllowCredentials(true);

๋Œ€์‘ ์š”์•ฝ: CORS๋Š” ์‹ ๋ขฐ ์ถœ์ฒ˜๋งŒ ํ—ˆ์šฉํ•˜๊ณ , ๋ถˆํ•„์š”ํ•œ HTTP ๋ฉ”์„œ๋“œ๋ฅผ ๋ง‰๋Š”๋‹ค.
๋ณด์•ˆ ํ—ค๋”๋ฅผ ์ ์šฉํ•˜๊ณ , ์—๋Ÿฌ ์‘๋‹ต์— ์ŠคํƒํŠธ๋ ˆ์ด์Šค๋‚˜ ๋‚ด๋ถ€ ์ •๋ณด๋ฅผ ๋…ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค.


API9. Improper Inventory Management — ๋ถ€์ ์ ˆํ•œ ์ธ๋ฒคํ† ๋ฆฌ ๊ด€๋ฆฌ

์–ด๋–ค API๊ฐ€ ์–ด๋””์„œ ๋Œ๊ณ  ์žˆ๋Š”์ง€ ํŒŒ์•…์ด ์•ˆ ๋˜๋Š” ๊ฒฝ์šฐ๋‹ค.
๋ฌธ์„œํ™” ์•ˆ ๋œ ์˜› ๋ฒ„์ „(/api/v1), ํ…Œ์ŠคํŠธ ์„œ๋ฒ„, ํ๊ธฐํ–ˆ์–ด์•ผ ํ•  ์—”๋“œํฌ์ธํŠธ๊ฐ€ ๋ฐฉ์น˜๋˜๋ฉฐ ๊ณต๊ฒฉ ํ‘œ๋ฉด์ด ๋œ๋‹ค.

์ทจ์•ฝ ์˜ˆ:
  /api/v3/users   ← ์ตœ์‹ , ๋ณด์•ˆ ํŒจ์น˜ ์ ์šฉ๋จ
  /api/v1/users   ← ์˜› ๋ฒ„์ „, ์ธ์ฆ ์•ฝํ•จ, ์•„์ง ์‚ด์•„ ์žˆ์Œ ← ๊ณต๊ฒฉ ๋Œ€์ƒ

๋Œ€์‘ ์š”์•ฝ: ๋ชจ๋“  API ์—”๋“œํฌ์ธํŠธ์™€ ๋ฒ„์ „์„ ๋ฌธ์„œํ™”(OpenAPI/Swagger)ํ•˜๊ณ  ๋ชฉ๋ก์„ ๊ด€๋ฆฌํ•œ๋‹ค.
๋” ์ด์ƒ ์•ˆ ์“ฐ๋Š” ๋ฒ„์ „์€ ๋ช…์‹œ์ ์œผ๋กœ ํ๊ธฐ(deprecate)ํ•˜๊ณ  ์ข…๋ฃŒํ•œ๋‹ค. ์šด์˜/ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๋ถ„๋ฆฌํ•œ๋‹ค.


API10. Unsafe Consumption of APIs — ์•ˆ์ „ํ•˜์ง€ ์•Š์€ API ์†Œ๋น„ (์‹ ๊ทœ)

๋‚ด๊ฐ€ ๋งŒ๋“  API๊ฐ€ ์•„๋‹ˆ๋ผ, ๋‚ด๊ฐ€ ํ˜ธ์ถœํ•˜๋Š” ์™ธ๋ถ€(์„œ๋“œํŒŒํ‹ฐ) API๋ฅผ ๋„ˆ๋ฌด ์‹ ๋ขฐํ•  ๋•Œ์˜ ์œ„ํ—˜์ด๋‹ค.
๊ณต๊ฒฉ์ž๋Š” ์ง์ ‘ ๊ณต๊ฒฉํ•˜๊ธฐ ์–ด๋ ค์šด ๋Œ€์ƒ ๋Œ€์‹ , ๊ทธ ๋Œ€์ƒ์ด ์—ฐ๋™ํ•œ ์™ธ๋ถ€ ์„œ๋น„์Šค๋ฅผ ๋…ธ๋ฆฐ๋‹ค.

// ์ทจ์•ฝ: ์™ธ๋ถ€ API ์‘๋‹ต์„ ๊ฒ€์ฆ ์—†์ด ๊ทธ๋Œ€๋กœ ์‹ ๋ขฐ
ExternalResponse res = restClient.get().uri(thirdPartyUrl)
        .retrieve().body(ExternalResponse.class);
processPayment(res.getAmount());   // ์™ธ๋ถ€ ์‘๋‹ต์„ ๊ทธ๋Œ€๋กœ ๊ฒฐ์ œ์— ์‚ฌ์šฉ
// ๋Œ€์‘: ์™ธ๋ถ€ ์‘๋‹ต๋„ ๋‚ด ์ž…๋ ฅ์ฒ˜๋Ÿผ ๊ฒ€์ฆ
ExternalResponse res = restClient.get().uri(thirdPartyUrl)
        .retrieve().body(ExternalResponse.class);
if (res.getAmount() == null || res.getAmount().signum() < 0) {
    throw new IntegrationException("์™ธ๋ถ€ ์‘๋‹ต์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค");
}
processPayment(res.getAmount());

๋Œ€์‘ ์š”์•ฝ: ์™ธ๋ถ€ API์™€๋„ TLS๋กœ๋งŒ ํ†ต์‹ ํ•˜๊ณ , ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ณผ ๋˜‘๊ฐ™์ด ๊ฒ€์ฆํ•œ๋‹ค. ์™ธ๋ถ€ ์„œ๋น„์Šค๋กœ์˜ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ๋งน๋ชฉ์ ์œผ๋กœ ๋”ฐ๋ผ๊ฐ€์ง€ ์•Š๋Š”๋‹ค.


๋งˆ์น˜๋ฉฐ

์›น Top 10๊ณผ API Top 10์„ ๋‚˜๋ž€ํžˆ ๋ณด๋ฉด ๊ฐ•์กฐ์ ์ด ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒŒ ๋ณด์ธ๋‹ค.
์›น์€ ์ธ์ ์…˜·์•”ํ˜ธํ™”·์„ค์ •์ด ๊ณจ๊ณ ๋ฃจ ๋ถ„ํฌํ•˜๋Š” ๋ฐ˜๋ฉด, API๋Š” ์ธ๊ฐ€(Authorization)์— ์œ„ํ—˜์ด ์ง‘์ค‘๋ผ ์žˆ๋‹ค.
API1·API3·API5๊ฐ€ ๋ชจ๋‘ "๊ถŒํ•œ ๊ฒ€์ฆ์„ ๋น ๋œจ๋ ค์„œ" ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ๋‹ค.

์ด์œ ๋Š” ๋‹จ์ˆœํ•˜๋‹ค. API๋Š” ํ™”๋ฉด(UI)์ด๋ผ๋Š” ๋ฐฉ์–ด๋ง‰ ์—†์ด ๋ฐ์ดํ„ฐ์— ์ง์ ‘ ์ ‘๊ทผํ•˜๋Š” ํ†ต๋กœ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
UI์—์„œ ๋ฒ„ํŠผ์„ ์ˆจ๊ธฐ๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” ์•„๋ฌด ์˜๋ฏธ๊ฐ€ ์—†๋‹ค.
๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด "์ด ์‚ฌ์šฉ์ž๊ฐ€ ์ด ๊ฐ์ฒด/๊ธฐ๋Šฅ์— ๊ถŒํ•œ์ด ์žˆ๋Š”๊ฐ€"๋ฅผ ์„œ๋ฒ„์—์„œ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ, ๊ทธ๊ฒŒ API ๋ณด์•ˆ์˜ 9ํ• ์ด๋‹ค.

์›น๊ณผ API, ๋‘ Top 10์„ ํ•จ๊ป˜ ์•Œ์•„๋‘๋ฉด ์„œ๋น„์Šค ์ „์ฒด์˜ ๊ณต๊ฒฉ ํ‘œ๋ฉด์ด ๋ณด์ธ๋‹ค. ํ”„๋ก ํŠธ์™€ ๋ฐฑ์—”๋“œ๊ฐ€ ๋ถ„๋ฆฌ๋œ ์š”์ฆ˜ ๊ตฌ์กฐ์—์„œ๋Š” ๋‘˜ ๋‹ค ์ฑ™๊ฒจ์•ผ ํ•œ๋‹ค.


์ฐธ๊ณ  ์ถœ์ฒ˜