๐Ÿ” OWASP Top 10:2025 ์™„์ „ ๊ฐ€์ด๋“œ: ์›น ๋ณด์•ˆ ์ทจ์•ฝ์ ๊ณผ ๋ฐฉ์–ด ์ „๋žต

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

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•˜๊ฑฐ๋‚˜ ์šด์˜ํ•  ๋•Œ "๋ณด์•ˆ"์€ ๋Š˜ ์šฐ์„ ์ˆœ์œ„์—์„œ ๋ฐ€๋ฆฌ๊ธฐ ์‰ฝ๋‹ค.
๊ธฐ๋Šฅ ๊ฐœ๋ฐœ์ด ๊ธ‰ํ•˜๊ณ , ์ผ์ •์ด ์ด‰๋ฐ•ํ•˜๊ณ , "์„ค๋งˆ ์šฐ๋ฆฌ ์„œ๋น„์Šค๋ฅผ ๊ณต๊ฒฉํ•˜๊ฒ ์–ด?" ํ•˜๋Š” ์•ˆ์ผํ•จ์ด ์ƒ๊ธด๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์‹ค์ œ ์นจํ•ด ์‚ฌ๊ณ ๋ฅผ ๋ณด๋ฉด ๋Œ€๋ถ€๋ถ„ ์ด๋ฏธ ์•Œ๋ ค์ง„ ์ทจ์•ฝ์ ์„ ํ†ตํ•ด ๋ฐœ์ƒํ•œ๋‹ค.
OWASP Top 10์€ ๊ทธ "์ด๋ฏธ ์•Œ๋ ค์ง„" ์ทจ์•ฝ์  ๋ชฉ๋ก์ด๋‹ค.

์ด ๊ธ€์—์„œ๋Š” 2025๋…„ ์ตœ์‹  ๋ฒ„์ „์„ ๊ธฐ์ค€์œผ๋กœ ๊ฐ ํ•ญ๋ชฉ์„ ์ฝ”๋“œ ์ˆ˜์ค€๊นŒ์ง€ ํŒŒ๊ณ ๋“ ๋‹ค.


OWASP๋ž€ ๋ฌด์—‡์ธ๊ฐ€

OWASP(Open Worldwide Application Security Project) ๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณด์•ˆ ํ–ฅ์ƒ์„ ์œ„ํ•œ ๋น„์˜๋ฆฌ ์˜คํ”ˆ์†Œ์Šค ์ปค๋ฎค๋‹ˆํ‹ฐ๋‹ค.
2001๋…„ ์„ค๋ฆฝ ์ดํ›„ ํ‘œ์ค€, ๊ฐ€์ด๋“œ๋ผ์ธ, ๋„๊ตฌ, ๋ฌธ์„œ๋ฅผ ๋ฌด๋ฃŒ๋กœ ๊ณต๊ฐœํ•ด์™”๋‹ค.

๊ทธ ์ค‘ OWASP Top 10 ์€ "์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ ๋ณด์•ˆ ์ทจ์•ฝ์  10๊ฐ€์ง€"๋ฅผ ์ •๋ฆฌํ•œ ๋ฌธ์„œ๋กœ,
์ „ ์„ธ๊ณ„ ์ˆ˜์ฒœ ๊ฐœ ์กฐ์ง์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•ด ์œ„ํ—˜๋„ ์ˆœ์œผ๋กœ ๋žญํ‚น์„ ๋งค๊ธด๋‹ค.

OWASP Top 10์ด ์ค‘์š”ํ•œ ์ด์œ :

  • PCI DSS, ISO 27001 ๋“ฑ ๊ตญ์ œ ๋ณด์•ˆ ์ธ์ฆ์˜ ๊ธฐ์ค€์œผ๋กœ ํ™œ์šฉ
  • ๊ฐœ๋ฐœํŒ€์˜ ๋ณด์•ˆ ์ธ์‹ ๊ต์œก ๊ต์žฌ๋กœ ์‚ฌ์šฉ
  • ์ทจ์•ฝ์  ์ ๊ฒ€ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์—ญํ• 
  • ๋ฉด์ ‘์—์„œ ๋ณด์•ˆ ์—ญ๋Ÿ‰์„ ๋ฌป๋Š” ๋‹จ๊ณจ ์งˆ๋ฌธ

OWASP Top 10์˜ ์—ญ์‚ฌ

๋ฒ„์ „ ์ฃผ์š” ๋ณ€ํ™”
2013 Injection, XSS, CSRF ์ค‘์‹ฌ
2017 XML External Entity (XXE), Insecure Deserialization ์‹ ์„ค
2021 Insecure Design, Software/Data Integrity ์‹ ์„ค. SSRF ์ฒ˜์Œ ๋“ฑ์žฅ
2025 Security Misconfiguration A02๋กœ ๊ธ‰์ƒ์Šน, Supply Chain ์‹ ์„ค, SSRF ์ œ๊ฑฐ

๋ฒ„์ „์ด ๋ฐ”๋€”์ˆ˜๋ก "๊ธฐ์ˆ ์ด ๋ฐœ์ „ํ•œ ๋ฐฉํ–ฅ"์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜์˜ํ•œ๋‹ค.
ํด๋ผ์šฐ๋“œ, CI/CD, ์˜คํ”ˆ์†Œ์Šค ์˜์กด๋„๊ฐ€ ๋†’์•„์ง€๋ฉด์„œ ๊ณต๊ธ‰๋ง ๊ณต๊ฒฉ์ด ๋ฉ”์ธ์œผ๋กœ ์˜ฌ๋ผ์˜จ ๊ฒƒ์ด 2025์˜ ํ•ต์‹ฌ์ด๋‹ค.


2021 โ†’ 2025, ๋ฌด์—‡์ด ๋‹ฌ๋ผ์กŒ๋‚˜

2021
2025
์ œ๊ฑฐ
A01 Broken Access Control
A02 Security Misconfiguration โฌ†๏ธ
A03 Supply Chain Failures ๐Ÿ†•
A04 Cryptographic Failures โฌ‡๏ธ
A05 Injection โฌ‡๏ธ
A06 Insecure Design โฌ‡๏ธ
A07 Auth Failures
A08 Integrity Failures
A09 Logging Failures
A10 Exceptional Conditions ๐Ÿ†•
A01 Broken Access Control
A02 Cryptographic Failures
A03 Injection
A04 Insecure Design
A05 Security Misconfiguration
A06 Vulnerable Components
A07 Auth Failures
A08 Integrity Failures
A09 Logging Failures
A10 SSRF

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

๊ตฌ๋ถ„ 2021 2025 ๋ณ€ํ™” ์ด์œ 
Broken Access Control A01 A01 ๋ณ€๋™ ์—†์Œ, 100% ์•ฑ์—์„œ ๋ฐœ๊ฒฌ
Security Misconfiguration A05 A02 ํด๋ผ์šฐ๋“œ/์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ ์ฆ๊ฐ€
Supply Chain Failures A06 (์ข์€ ๋ฒ”์œ„) A03 Log4Shell, SolarWinds ์ถฉ๊ฒฉ
Cryptographic Failures A02 A04 ์ƒ๋Œ€์  ํ•˜๋ฝ
Injection A03 A05 ์ƒ๋Œ€์  ํ•˜๋ฝ (ORM ๋ณดํŽธํ™”)
SSRF A10 ์ œ๊ฑฐ ๋ฒ”์ฃผ ์žฌํŽธ
Exceptional Conditions ์—†์Œ A10 ์ƒˆ๋กœ ์‹ ์„ค

A01:2025 โ€” Broken Access Control

๊ฐœ์š”

์ ‘๊ทผ ์ œ์–ด ์‹คํŒจ๋Š” 2021์— ์ด์–ด 2025์—์„œ๋„ 1์œ„๋ฅผ ์œ ์ง€ํ–ˆ๋‹ค.
ํ…Œ์ŠคํŠธ๋œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ 100%์—์„œ ๋ฐœ๊ฒฌ๋  ๋งŒํผ ์‚ฌ์‹ค์ƒ ๋ชจ๋“  ์•ฑ์— ์กด์žฌํ•œ๋‹ค.

์ ‘๊ทผ ์ œ์–ด๋Š” "์ด ์‚ฌ์šฉ์ž๊ฐ€ ์ด ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€"๋ฅผ ํŒ๋‹จํ•˜๋Š” ๋กœ์ง์ด๋‹ค.
์ด๊ฒŒ ๊นจ์ง€๋ฉด ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๊ฑฐ๋‚˜, ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ์„ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๊ฐ€ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

์ทจ์•ฝํ•œ ํŒจํ„ด๋“ค

โ‘  IDOR (Insecure Direct Object Reference) โ€” URL ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ž‘

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ์ธ์ฆ์€ ํ–ˆ์ง€๋งŒ ์†Œ์œ ๊ถŒ ๊ฒ€์ฆ ์—†์Œ
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await Order.findById(req.params.orderId)
  res.json(order) // ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ์ฃผ๋ฌธ๋„ ์กฐํšŒ ๊ฐ€๋Šฅ
})

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ์š”์ฒญ์ž ์†Œ์œ  ์—ฌ๋ถ€ ๊ฒ€์ฆ
app.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(403).json({ error: 'Forbidden' })
  res.json(order)
})

โ‘ก ๊ฐ•์ œ ํƒ์ƒ‰ (Forced Browsing) โ€” URL ์ง์ ‘ ์ ‘๊ทผ

# ๊ณต๊ฒฉ์ž๊ฐ€ ์ง์ ‘ URL์„ ์ถ”์ธกํ•ด์„œ ์ ‘๊ทผ ์‹œ๋„
GET /admin/users
GET /admin/dashboard
GET /internal/reports
// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ URL๋งŒ ์ˆจ๊น€ (Security by Obscurity)
app.get('/admin-panel-xk3f', (req, res) => {
  // URL ๋ชจ๋ฅด๋ฉด ๋ชป ๋“ค์–ด์˜ค๊ฒ ์ง€... (ํ‹€๋ฆฐ ์ƒ๊ฐ)
  res.render('admin')
})

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด
app.get('/admin', authenticate, requireRole('ADMIN'), (req, res) => {
  res.render('admin')
})

function requireRole(role) {
  return (req, res, next) => {
    if (!req.user.roles.includes(role)) {
      return res.status(403).json({ error: 'Insufficient privileges' })
    }
    next()
  }
}

โ‘ข JWT ํ† ํฐ ์กฐ์ž‘

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: alg=none ๊ณต๊ฒฉ์— ์ทจ์•ฝ
const decoded = jwt.verify(token, secret, { algorithms: ['HS256', 'none'] })

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ํ—ˆ์šฉ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ช…์‹œ์  ์ œํ•œ
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] })

๋ฐฉ์–ด ์ „๋žต

  • Deny by default: ๋ช…์‹œ์ ์œผ๋กœ ํ—ˆ์šฉ๋œ ๊ฒฝ์šฐ์—๋งŒ ์ ‘๊ทผ ํ—ˆ์šฉ
  • ์„œ๋ฒ„ ์‚ฌ์ด๋“œ์—์„œ๋งŒ ์ ‘๊ทผ ์ œ์–ด ํŒ๋‹จ (ํด๋ผ์ด์–ธํŠธ hidden ํ•„๋“œ ๋ฏฟ์ง€ ์•Š๊ธฐ)
  • ๋ฆฌ์†Œ์Šค CRUD ์‹œ ์†Œ์œ ๊ถŒ ๊ฒ€์ฆ ํ•„์ˆ˜
  • ์งง์€ ์œ ํšจ๊ธฐ๊ฐ„์˜ JWT + ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ
  • API ์—”๋“œํฌ์ธํŠธ Rate Limiting ์ ์šฉ

A02:2025 โ€” Security Misconfiguration โฌ†๏ธ

๊ฐœ์š”

2021๋…„ A05์—์„œ A02๋กœ 3๋‹จ๊ณ„ ๊ธ‰์ƒ์Šนํ–ˆ๋‹ค.
ํ…Œ์ŠคํŠธ๋œ ์•ฑ์˜ 100%, ์ „์ฒด ๋ฐœ์ƒ ๊ฑด์ˆ˜ 71๋งŒ 9์ฒœ ๊ฑด ์ด์ƒ์œผ๋กœ ์‚ฌ์‹ค์ƒ ๊ฐ€์žฅ ํ”ํ•œ ์ทจ์•ฝ์ ์ด๋‹ค.

ํด๋ผ์šฐ๋“œ, ์ปจํ…Œ์ด๋„ˆ, ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ™˜๊ฒฝ์ด ๋ณดํŽธํ™”๋˜๋ฉด์„œ ์„ค์ • ํฌ์ธํŠธ ์ˆ˜๊ฐ€ ํญ๋ฐœ์ ์œผ๋กœ ๋Š˜์—ˆ๋‹ค.
์„ค์ • ํ•˜๋‚˜ ์ž˜๋ชป๋˜๋ฉด ์ „์ฒด ์ธํ”„๋ผ๊ฐ€ ๋…ธ์ถœ๋œ๋‹ค.

๋Œ€ํ‘œ์ ์ธ ์ž˜๋ชป๋œ ์„ค์ •

โ‘  ๊ธฐ๋ณธ ์ž๊ฒฉ ์ฆ๋ช… ๋ฏธ๋ณ€๊ฒฝ

# ์ทจ์•ฝํ•œ ์˜ˆ์‹œ: ์ œํ’ˆ ๊ธฐ๋ณธ๊ฐ’ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
database:
  host: localhost
  username: admin
  password: admin  # ๋˜๋Š” password, 123456

# MongoDB, Redis, Elasticsearch ๋“ฑ์€ ๊ธฐ๋ณธ ์„ค์ •์ด ์ธ์ฆ ์—†์Œ
# โ†’ ํฌํŠธ๋งŒ ์—ด๋ ค์žˆ์œผ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ์ ‘๊ทผ ๊ฐ€๋Šฅ

โ‘ก ๋ถˆํ•„์š”ํ•œ ํฌํŠธ/์„œ๋น„์Šค ์˜คํ”ˆ

# ๊ฐœ๋ฐœ ์ค‘์— ์—ด์–ด๋‘” ๋””๋ฒ„๊ทธ ํฌํŠธ๊ฐ€ ํ”„๋กœ๋•์…˜์—๋„ ์—ด๋ฆผ
# Node.js ๋””๋ฒ„๊ทธ ํฌํŠธ
--inspect=0.0.0.0:9229  # ์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋ฉด ์›๊ฒฉ ์ฝ”๋“œ ์‹คํ–‰ ๊ฐ€๋Šฅ

# ์ž ์žฌ์ ์œผ๋กœ ์œ„ํ—˜ํ•œ ํฌํŠธ๋“ค
22   # SSH (๊ธฐ๋ณธ ํฌํŠธ)
3306 # MySQL
5432 # PostgreSQL
6379 # Redis
27017 # MongoDB

โ‘ข ์ƒ์„ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋…ธ์ถœ

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ
app.use((err, req, res, next) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack,  // DB ๊ตฌ์กฐ, ํŒŒ์ผ ๊ฒฝ๋กœ, ๋‚ด๋ถ€ ๋กœ์ง ๋…ธ์ถœ
    query: err.query   // SQL ์ฟผ๋ฆฌ๊นŒ์ง€ ๋…ธ์ถœ
  })
})

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ์ผ๋ฐ˜ํ™”๋œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
app.use((err, req, res, next) => {
  logger.error(err) // ๋‚ด๋ถ€ ๋กœ๊ทธ์—๋งŒ ๊ธฐ๋ก
  res.status(500).json({ error: 'Internal server error' })
})

โ‘ฃ HTTP ๋ณด์•ˆ ํ—ค๋” ๋ˆ„๋ฝ

# ์ทจ์•ฝํ•œ ์„ค์ •: ๋ณด์•ˆ ํ—ค๋” ์—†์Œ

# ์•ˆ์ „ํ•œ ์„ค์ •
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
add_header Content-Security-Policy "default-src 'self'";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Permissions-Policy "geolocation=(), microphone=()";

๋ฐฉ์–ด ์ „๋žต

  • IaC(Infrastructure as Code) ๋กœ ํ™˜๊ฒฝ ์„ค์ • ์ฝ”๋“œํ™” + ์ฝ”๋“œ ๋ฆฌ๋ทฐ
  • ์ตœ์†Œํ•œ์˜ ์„ค์น˜ ์›์น™: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ/์„œ๋น„์Šค/ํฌํŠธ๋Š” ์ œ๊ฑฐ
  • ๊ฐœ๋ฐœ/์Šคํ…Œ์ด์ง•/ํ”„๋กœ๋•์…˜ ์„ค์ • ์™„์ „ ๋ถ„๋ฆฌ
  • ์ •๊ธฐ์ ์ธ ์„ค์ • ๊ฐ์‚ฌ (CIS Benchmark, CSPM ๋„๊ตฌ ํ™œ์šฉ)
  • ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ํ”„๋กœ๋•์…˜ ๋…ธ์ถœ ๊ธˆ์ง€

A03:2025 โ€” Software Supply Chain Failures ๐Ÿ†•

๊ฐœ์š”

2021๋…„ "์ทจ์•ฝํ•˜๊ฑฐ๋‚˜ ์˜ค๋ž˜๋œ ์ปดํฌ๋„ŒํŠธ(A06)"์—์„œ ๋ฒ”์œ„๊ฐ€ ํฌ๊ฒŒ ํ™•๋Œ€๋œ ์‹ ์„ค ์นดํ…Œ๊ณ ๋ฆฌ๋‹ค.
๋‹จ์ˆœํžˆ "ํŒจ์น˜ ์•ˆ ๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ"๋ฅผ ๋„˜์–ด์„œ, ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ๋งŒ๋“ค๊ณ  ๋ฐฐํฌํ•˜๋Š” ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ์ด ๊ณต๊ฒฉ ๋Œ€์ƒ์ด ๋๋‹ค.

์ปค๋ฎค๋‹ˆํ‹ฐ ์„ค๋ฌธ์—์„œ 1์œ„ ์œ„ํ—˜์œผ๋กœ 50% ๋“ํ‘œ, ํ‰๊ท  ๋ฐœ์ƒ๋ฅ  5.19% ๋กœ ๊ฐ€์žฅ ๋†’๋‹ค.

์‹ค์ œ ์‚ฌ๋ก€

์•…์„ฑ ์ฝ”๋“œ ์ฃผ์ž…
์ •์ƒ ๋ฐฐํฌ ์ฑ„๋„
ํ…Œ์ŠคํŠธ ํ†ต๊ณผ
์ž๋™ ๋ฐฐํฌ
ํ”ผํ•ด ๋ฐœ์ƒ
โš ๏ธ ๊ณต๊ฒฉ์ž
์˜คํ”ˆ์†Œ์Šค ํŒจํ‚ค์ง€ / ๋นŒ๋“œ ์„œ๋ฒ„
๊ฐœ๋ฐœํŒ€: npm install / pip install
CI/CD ํŒŒ์ดํ”„๋ผ์ธ
ํ”„๋กœ๋•์…˜ ์„œ๋ฒ„
๐Ÿ’€ ๋ฐ์ดํ„ฐ ํƒˆ์ทจ ยท ๋ฐฑ๋„์–ด ยท ๋žœ์„ฌ์›จ์–ด

SolarWinds (2020):
Orion ์†Œํ”„ํŠธ์›จ์–ด์˜ ๋นŒ๋“œ ์‹œ์Šคํ…œ์— ์•…์„ฑ ์ฝ”๋“œ๊ฐ€ ์ฃผ์ž…๋จ.
18,000๊ฐœ ์ด์ƒ ์กฐ์ง์ด ์ •์ƒ ์—…๋ฐ์ดํŠธ ํŒŒ์ผ์„ ๋ฐ›์•„ ์„ค์น˜ โ†’ ๋ฐฑ๋„์–ด ์„ค์น˜.
๋ฏธ๊ตญ ์ •๋ถ€ ๊ธฐ๊ด€ ๋‹ค์ˆ˜ ํฌํ•จ.

Log4Shell (CVE-2021-44228):
์ž๋ฐ” ๋กœ๊น… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ Log4j์˜ ์ทจ์•ฝ์ .
์ „ ์„ธ๊ณ„ ์ˆ˜๋ฐฑ๋งŒ ๊ฐœ ์„œ๋ฒ„๊ฐ€ ๋‹จ ํ•œ ์ค„์˜ ๋กœ๊ทธ ์ž…๋ ฅ์œผ๋กœ ์›๊ฒฉ ์ฝ”๋“œ ์‹คํ–‰์— ๋…ธ์ถœ.

npm Shai-Hulud ์›œ (2025):
์ž๊ธฐ ๋ณต์ œ ์•…์„ฑ npm ํŒจํ‚ค์ง€. 500๊ฐœ ์ด์ƒ ํŒจํ‚ค์ง€ ๋ฒ„์ „ ๊ฐ์—ผ, ๊ฐœ๋ฐœ์ž ํ† ํฐ์„ ํƒˆ์ทจํ•ด ์ถ”๊ฐ€ ํ™•์‚ฐ.

์ทจ์•ฝํ•œ ํŒจํ„ด

// package.json โ€” ๋ฒ„์ „ ๊ณ ์ • ์—†์ด ์ตœ์‹  ๋ฒ„์ „ ์ž๋™ ์„ค์น˜
{
  "dependencies": {
    "express": "*",       // ์œ„ํ—˜: ์•…์˜์  ์—…๋ฐ์ดํŠธ ์ž๋™ ์„ค์น˜
    "lodash": "^4.0.0",  // ์ฃผ์˜: ๋งˆ์ด๋„ˆ ๋ฒ„์ „ ์ž๋™ ์—…๊ทธ๋ ˆ์ด๋“œ
    "axios": "~1.0.0"    // ํŒจ์น˜ ๋ฒ„์ „๋งŒ ์ž๋™ ์—…๊ทธ๋ ˆ์ด๋“œ
  }
}
# ์˜์กด์„ฑ ์ทจ์•ฝ์  ํ™•์ธ
npm audit
pip-audit
trivy image my-app:latest

# SBOM(Software Bill of Materials) ์ƒ์„ฑ
syft my-app:latest -o spdx-json > sbom.json

๋ฐฉ์–ด ์ „๋žต

  • SBOM ๊ด€๋ฆฌ: ๋ชจ๋“  ์˜์กด์„ฑ ๋ชฉ๋กํ™”, ๋ฒ„์ „ ์ถ”์ 
  • ์˜์กด์„ฑ ๋ฒ„์ „ ๊ณ ์ •: package-lock.json, requirements.txt ์ •ํ™•ํ•œ ๋ฒ„์ „ ๋ช…์‹œ
  • ์ •๊ธฐ ์ทจ์•ฝ์  ์Šค์บ”: Dependabot, Snyk, Trivy
  • CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๋ณด์•ˆ: ๋นŒ๋“œ ํ™˜๊ฒฝ ๊ฒฉ๋ฆฌ, ์„œ๋ช… ๊ฒ€์ฆ, MFA ์ ์šฉ
  • ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋งŒ ์‚ฌ์šฉ: ์‚ฌ๋‚ด ๋ฏธ๋Ÿฌ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์šด์˜
  • ๋‹จ๊ณ„์  ๋ฐฐํฌ(Canary): ์ทจ์•ฝํ•œ ์—…๋ฐ์ดํŠธ ํ”ผํ•ด ๋ฒ”์œ„ ์ตœ์†Œํ™”

A04:2025 โ€” Cryptographic Failures

๊ฐœ์š”

2021๋…„ A02์—์„œ A04๋กœ ํ•˜๋ฝํ–ˆ์ง€๋งŒ ์—ฌ์ „ํžˆ ์‹ฌ๊ฐํ•˜๋‹ค.
"์•”ํ˜ธํ™” ์‹คํŒจ"๋Š” ๋‹จ์ˆœํžˆ "์•”ํ˜ธํ™” ์•ˆ ํ•จ"๋ฟ ์•„๋‹ˆ๋ผ ์ž˜๋ชป๋œ ์•”ํ˜ธํ™” ์‚ฌ์šฉ๋„ ํฌํ•จํ•œ๋‹ค.

์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์‹ค์ˆ˜

โ‘  ๋น„๋ฐ€๋ฒˆํ˜ธ ํ‰๋ฌธ ์ €์žฅ ๋˜๋Š” ์•ฝํ•œ ํ•ด์‹œ

import hashlib
import bcrypt

# ์ทจ์•ฝํ•œ ์ฝ”๋“œ: MD5, SHA1 ์‚ฌ์šฉ (๋ ˆ์ธ๋ณด์šฐ ํ…Œ์ด๋ธ” ๊ณต๊ฒฉ์— ์ทจ์•ฝ)
def bad_hash_password(password):
    return hashlib.md5(password.encode()).hexdigest()

def also_bad(password):
    return hashlib.sha1(password.encode()).hexdigest()

# ์•ˆ์ „ํ•œ ์ฝ”๋“œ: bcrypt (์†”ํŠธ ์ž๋™ ํฌํ•จ, ๋А๋ฆฐ ํ•ด์‹œ)
def good_hash_password(password):
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

def verify_password(password, hashed):
    return bcrypt.checkpw(password.encode(), hashed)

โ‘ก HTTP๋กœ ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ ์ „์†ก

# ์ทจ์•ฝ: ๋กœ๊ทธ์ธ ํผ์ด HTTP๋กœ ์ „์†ก
POST http://example.com/login
Content-Type: application/x-www-form-urlencoded

username=admin&password=secret123  # ํ‰๋ฌธ์œผ๋กœ ๋„คํŠธ์›Œํฌ์— ๋…ธ์ถœ

# ์•ˆ์ „: ๋ฐ˜๋“œ์‹œ HTTPS
POST https://example.com/login

โ‘ข ์ทจ์•ฝํ•œ TLS ์„ค์ •

# ์ทจ์•ฝํ•œ ์„ค์ •: ๊ตฌ๋ฒ„์ „ TLS ํ—ˆ์šฉ
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;  # TLS 1.0, 1.1์€ POODLE, BEAST ๊ณต๊ฒฉ์— ์ทจ์•ฝ

# ์•ˆ์ „ํ•œ ์„ค์ •
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers off;

โ‘ฃ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์•”ํ˜ธํ™” ํ‚ค

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ํ‚ค๊ฐ€ ์†Œ์Šค ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉ
private static final String SECRET_KEY = "my-super-secret-key-123";

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋˜๋Š” Secrets Manager์—์„œ ๋กœ๋“œ
private static final String SECRET_KEY = System.getenv("JWT_SECRET");

๋ฐฉ์–ด ์ „๋žต

์ƒํ™ฉ ๊ถŒ์žฅ ์•Œ๊ณ ๋ฆฌ์ฆ˜
๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ bcrypt, Argon2, scrypt
๋Œ€์นญ ์•”ํ˜ธํ™” AES-256-GCM
๋น„๋Œ€์นญ ์•”ํ˜ธํ™” RSA-2048+, ECDSA
ํ•ด์‹œ (์ผ๋ฐ˜) SHA-256, SHA-3
TLS 1.2 ์ด์ƒ, 1.3 ๊ถŒ์žฅ
์‚ฌ์šฉ ๊ธˆ์ง€ MD5, SHA1, DES, ECB ๋ชจ๋“œ
  • ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ๋Š” ํ•„์š”ํ•œ ๊ธฐ๊ฐ„๋งŒ ์ €์žฅ (๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์ฆ‰์‹œ ์‚ญ์ œ)
  • ํ‚ค๋Š” ์ฝ”๋“œ๊ฐ€ ์•„๋‹Œ Vault, AWS Secrets Manager, ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ

A05:2025 โ€” Injection

๊ฐœ์š”

2021๋…„ A03์—์„œ A05๋กœ ํ•˜๋ฝํ–ˆ๋‹ค. ORM ์‚ฌ์šฉ์ด ๋ณดํŽธํ™”๋˜๋ฉด์„œ ์ „ํ†ต์ ์ธ SQL Injection์ด ์ค„์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
ํ•˜์ง€๋งŒ SQL ์™ธ์—๋„ ๋‹ค์–‘ํ•œ Injection ๊ณต๊ฒฉ์ด ์กด์žฌํ•˜๋ฉฐ ์—ฌ์ „ํžˆ ์œ„ํ—˜ํ•˜๋‹ค.

SQL Injection

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์ฟผ๋ฆฌ์— ์ง์ ‘ concatenation
String query = "SELECT * FROM users WHERE name = '" + username + "'";
// ๊ณต๊ฒฉ์ž ์ž…๋ ฅ: admin' OR '1'='1
// ์‹คํ–‰๋˜๋Š” ์ฟผ๋ฆฌ: SELECT * FROM users WHERE name = 'admin' OR '1'='1'
// โ†’ ๋ชจ๋“  ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: PreparedStatement ์‚ฌ์šฉ
String query = "SELECT * FROM users WHERE name = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, username);

Command Injection

import subprocess
import shlex

# ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ์…ธ ๋ช…๋ น์— ์ง์ ‘ ์‚ฝ์ž…
def get_file_info(filename):
    output = subprocess.run(f"ls -la {filename}", shell=True, capture_output=True)
    return output.stdout
# ๊ณต๊ฒฉ์ž ์ž…๋ ฅ: "file.txt; cat /etc/passwd"
# ์‹คํ–‰: ls -la file.txt; cat /etc/passwd  โ†’ ์‹œ์Šคํ…œ ํŒŒ์ผ ๋…ธ์ถœ

# ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ์ „๋‹ฌ, shell=False
def get_file_info(filename):
    output = subprocess.run(["ls", "-la", filename], shell=False, capture_output=True)
    return output.stdout

SSTI (Server-Side Template Injection)

from flask import Flask, render_template_string

app = Flask(__name__)

# ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ์ง์ ‘ ๋ Œ๋”๋ง
@app.route('/greet')
def greet():
    name = request.args.get('name')
    return render_template_string(f"Hello {name}!")
# ๊ณต๊ฒฉ์ž ์ž…๋ ฅ: {{7*7}} โ†’ "Hello 49!" ์ถœ๋ ฅ (์ฝ”๋“œ ์‹คํ–‰ ํ™•์ธ)
# ์‹ฌํ™” ๊ณต๊ฒฉ: {{config.items()}} โ†’ ์„œ๋ฒ„ ์„ค์ • ๋…ธ์ถœ

# ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ๋ณ€์ˆ˜ ๋ถ„๋ฆฌ
@app.route('/greet')
def greet():
    name = request.args.get('name')
    return render_template_string("Hello {{ name }}!", name=name)

๋ฐฉ์–ด ์ „๋žต

  • Parameterized Query / PreparedStatement ์‚ฌ์šฉ (SQL)
  • ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ฐฉ์‹์œผ๋กœ ๊ฒ€์ฆ
  • ORM ์‚ฌ์šฉ ์‹œ์—๋„ Raw Query ์‚ฌ์šฉ ๊ธˆ์ง€
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ •์˜ DB ๊ถŒํ•œ ์ตœ์†Œํ™” (SELECT๋งŒ ํ•„์š”ํ•˜๋ฉด SELECT๋งŒ)
  • WAF(Web Application Firewall) ์ถ”๊ฐ€ ๋ ˆ์ด์–ด๋กœ ํ™œ์šฉ

A06:2025 โ€” Insecure Design

๊ฐœ์š”

2021๋…„์— ์‹ ์„ค๋œ ํ•ญ๋ชฉ์œผ๋กœ A04์—์„œ A06์œผ๋กœ ํ•˜๋ฝํ–ˆ๋‹ค.
์ด ํ•ญ๋ชฉ์€ ์ฝ”๋“œ๊ฐ€ ์•„๋‹Œ ์„ค๊ณ„ ๋‹จ๊ณ„์˜ ๋ฌธ์ œ๋ฅผ ๋‹ค๋ฃฌ๋‹ค.
๊ตฌํ˜„์„ ์™„๋ฒฝํ•˜๊ฒŒ ํ•ด๋„, ์„ค๊ณ„ ์ž์ฒด๊ฐ€ ์ž˜๋ชป๋์œผ๋ฉด ๋ณด์•ˆ์ด ๊นจ์ง„๋‹ค.

๋Œ€ํ‘œ ์‚ฌ๋ก€

๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • โ€” ์ทจ์•ฝํ•œ ํ”Œ๋กœ์šฐ ์„ค๊ณ„

์ •๋‹ต
์‚ฌ์šฉ์ž: ๋น„๋ฒˆ ์ฐพ๊ธฐ
์ด๋ฉ”์ผ ์ž…๋ ฅ
๋ณด์•ˆ ์งˆ๋ฌธ ํ™•์ธ

'์–ด๋จธ๋‹ˆ ์„ฑํ•จ์€?'
๋น„๋ฐ€๋ฒˆํ˜ธ ์ฆ‰์‹œ ์ด๋ฉ”์ผ ๋ฐœ์†ก

๋ฌธ์ œ์ :

  • ๋ณด์•ˆ ์งˆ๋ฌธ์€ ์†Œ์…œ ์—”์ง€๋‹ˆ์–ด๋ง์œผ๋กœ ์‰ฝ๊ฒŒ ํƒˆ์ทจ ๊ฐ€๋Šฅ
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ์ง์ ‘ ๋ฐœ์†ก = ํ‰๋ฌธ ์ €์žฅ ์ฆ๊ฑฐ
  • ์ด๋ฉ”์ผ ๊ณ„์ • ํƒˆ์ทจ ์‹œ ์ฆ‰์‹œ ๊ณ„์ • ํƒˆ์ทจ

์•ˆ์ „ํ•œ ์„ค๊ณ„:

์‚ฌ์šฉ์ž: ๋น„๋ฒˆ ์ฐพ๊ธฐ
์ด๋ฉ”์ผ ์ž…๋ ฅ
๋‹จ๊ธฐ ํ† ํฐ ์ƒ์„ฑ

15๋ถ„ ์œ ํšจ
์ด๋ฉ”์ผ๋กœ ์žฌ์„ค์ • ๋งํฌ ๋ฐœ์†ก
๋งํฌ ํด๋ฆญ โ†’ ํ† ํฐ ๊ฒ€์ฆ
์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •
๋ชจ๋“  ์„ธ์…˜ ๋ฌดํšจํ™”

๋ฐฉ์–ด ์ „๋žต

  • ๊ฐœ๋ฐœ ์ „ Threat Modeling ์ˆ˜ํ–‰ (STRIDE, PASTA ๋ฐฉ๋ฒ•๋ก )
  • Evil User Story ์ž‘์„ฑ: "๊ณต๊ฒฉ์ž๋ผ๋ฉด ์–ด๋–ป๊ฒŒ ์•…์šฉํ• ๊นŒ?"
  • ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹, ๊ณ„์ • ์ž ๊ธˆ ์ •์ฑ…์„ ์„ค๊ณ„ ๋‹จ๊ณ„์—์„œ ํฌํ•จ
  • Secure Design Patterns ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ฐธ์กฐ (OWASP Proactive Controls)

A07:2025 โ€” Authentication Failures

๊ฐœ์š”

์ธ์ฆ ์‹คํŒจ. 2021๋…„ "์‹๋ณ„ ๋ฐ ์ธ์ฆ ์‹คํŒจ(A07)"์—์„œ "์ธ์ฆ ์‹คํŒจ(A07)"๋กœ ์ด๋ฆ„์ด ์ •๋ฆฌ๋๋‹ค.
๋กœ๊ทธ์ธ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ์ž์ฒด์˜ ๊ฒฐํ•จ์„ ๋‹ค๋ฃฌ๋‹ค.

์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ํŒจํ„ด

โ‘  Credential Stuffing ๋ฐฉ์–ด ์—†์Œ

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: Rate Limit ์—†์Œ
app.post('/login', async (req, res) => {
  const { username, password } = req.body
  const user = await User.findOne({ username })
  if (user && await bcrypt.compare(password, user.password)) {
    res.json({ token: generateToken(user) })
  } else {
    res.status(401).json({ error: 'Invalid credentials' })
  }
})

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: Rate Limit + ๊ณ„์ • ์ž ๊ธˆ
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15๋ถ„
  max: 5,                    // 5ํšŒ ์ดˆ๊ณผ ์‹œ ์ฐจ๋‹จ
  message: 'Too many login attempts'
})

app.post('/login', loginLimiter, async (req, res) => {
  // ... ๋™์ผ ๋กœ์ง
})

โ‘ก ์„ธ์…˜ ID ์žฌ์‚ฌ์šฉ (Session Fixation)

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ๋กœ๊ทธ์ธ ํ›„ ์„ธ์…˜ ID ๊ฐฑ์‹  ์—†์Œ
app.post('/login', (req, res) => {
  req.session.userId = user.id  // ๊ธฐ์กด ์„ธ์…˜ ID ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
})

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ์„ธ์…˜ ์žฌ์ƒ์„ฑ
app.post('/login', (req, res) => {
  req.session.regenerate((err) => {  // ์ƒˆ ์„ธ์…˜ ID ๋ฐœ๊ธ‰
    req.session.userId = user.id
    res.json({ success: true })
  })
})

โ‘ข ๋น„๋ฐ€๋ฒˆํ˜ธ ์ •์ฑ… ์—†์Œ

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณต์žก๋„ ๊ฒ€์ฆ ์—†์Œ
app.post('/register', (req, res) => {
  const { password } = req.body
  // "1234", "aaaa" ๊ฐ™์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋„ ํ—ˆ์šฉ

  // ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ๋ณต์žก๋„ ๊ฒ€์ฆ
  const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
  if (!passwordRegex.test(password)) {
    return res.status(400).json({ error: '๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ, ๋Œ€์†Œ๋ฌธ์ž, ์ˆซ์ž, ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ ํ•„์š”' })
  }
})

๋ฐฉ์–ด ์ „๋žต

  • MFA(๋‹ค์ค‘ ์ธ์ฆ) ํ•„์ˆ˜ ์ ์šฉ (ํŠนํžˆ ๊ด€๋ฆฌ์ž ๊ณ„์ •)
  • ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ ๋™์ผํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ("์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค")
  • ์„ธ์…˜ ์ฟ ํ‚ค: HttpOnly, Secure, SameSite=Strict
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณต์žก๋„ ์ •์ฑ… + HIBP(Have I Been Pwned) API๋กœ ์œ ์ถœ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์‚ฌ
  • ์žฅ์‹œ๊ฐ„ ๋ฏธ์‚ฌ์šฉ ์„ธ์…˜ ์ž๋™ ๋งŒ๋ฃŒ

A08:2025 โ€” Software or Data Integrity Failures

๊ฐœ์š”

์†Œํ”„ํŠธ์›จ์–ด๋‚˜ ๋ฐ์ดํ„ฐ์˜ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ ์—†์ด ์‚ฌ์šฉํ•  ๋•Œ ๋ฐœ์ƒํ•œ๋‹ค.
2021๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€๋๋‹ค.

๋Œ€ํ‘œ ์‚ฌ๋ก€

โ‘  CI/CD ํŒŒ์ดํ”„๋ผ์ธ ์ทจ์•ฝ์ 

# ์ทจ์•ฝํ•œ GitHub Actions: ์™ธ๋ถ€ ์•ก์…˜ ๋ฒ„์ „ ๊ณ ์ • ์—†์Œ
steps:
  - uses: actions/checkout@main  # ์œ„ํ—˜: main ๋ธŒ๋žœ์น˜๋Š” ์–ธ์ œ๋“  ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ

# ์•ˆ์ „ํ•œ ์„ค์ •: ์ปค๋ฐ‹ ํ•ด์‹œ๋กœ ๊ณ ์ •
steps:
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

โ‘ก ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ ์—†๋Š” ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ

# ์ทจ์•ฝํ•œ ๋ฐฉ๋ฒ•: ๊ฒ€์ฆ ์—†์ด ์‹คํ–‰
curl https://example.com/install.sh | bash

# ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•: ํ•ด์‹œ ๊ฒ€์ฆ ํ›„ ์‹คํ–‰
curl -O https://example.com/install.sh
curl -O https://example.com/install.sh.sha256
sha256sum -c install.sh.sha256  # ํ•ด์‹œ ์ผ์น˜ ํ™•์ธ ํ›„
bash install.sh

โ‘ข ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ์—ญ์ง๋ ฌํ™”

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ์‹ ๋ขฐํ•  ์ˆ˜ ์—†๋Š” ์†Œ์Šค์—์„œ ์—ญ์ง๋ ฌํ™”
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject();  // ์•…์˜์  ์ง๋ ฌํ™” ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ RCE ๊ฐ€๋Šฅ

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ์—ญ์ง๋ ฌํ™” ์ „ ํƒ€์ž… ๊ฒ€์ฆ ๋˜๋Š” JSON ์‚ฌ์šฉ
// Java Deserialization Filter (Java 9+)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.example.SafeClass;!*"
);
ois.setObjectInputFilter(filter);

๋ฐฉ์–ด ์ „๋žต

  • ์™ธ๋ถ€ ์•ก์…˜/ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ์ปค๋ฐ‹ ํ•ด์‹œ๋กœ ๋ฒ„์ „ ๊ณ ์ •
  • ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ์€ ์ฒดํฌ์„ฌ(SHA-256) ๊ฒ€์ฆ
  • ์—ญ์ง๋ ฌํ™” ์‹œ ํƒ€์ž… ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ์ ์šฉ
  • ์ฝ”๋“œ ์„œ๋ช…(Code Signing) ์œผ๋กœ ๋ฐฐํฌ๋ฌผ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ
  • CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ๋น„๋ฐ€ ๊ฐ’ ์Šค์บ” ์ถ”๊ฐ€ (gitleaks, truffleHog)

A09:2025 โ€” Security Logging and Alerting Failures

๊ฐœ์š”

๋กœ๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง์˜ ์‹คํŒจ. 2021๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€๋๋‹ค.
์ด ํ•ญ๋ชฉ์€ ๊ณต๊ฒฉ ์ž์ฒด๋ฅผ ๋ง‰๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ๊ณต๊ฒฉ์„ ํƒ์ง€ํ•˜๊ณ  ๋Œ€์‘ํ•˜๋Š” ์—ญ๋Ÿ‰์— ๊ด€ํ•œ ๊ฒƒ์ด๋‹ค.

ํ‰๊ท  ์นจํ•ด ์‚ฌ๊ณ  ํƒ์ง€๊นŒ์ง€ 200์ผ ์ด์ƒ ๊ฑธ๋ฆฐ๋‹ค๋Š” ํ†ต๊ณ„๊ฐ€ ์žˆ๋‹ค.
๋กœ๊ทธ๊ฐ€ ์—†์œผ๋ฉด ๋šซ๋ ธ๋Š”์ง€์กฐ์ฐจ ๋ชจ๋ฅธ๋‹ค.

๋กœ๊ทธ์— ๋ฐ˜๋“œ์‹œ ๊ธฐ๋กํ•ด์•ผ ํ•  ๊ฒƒ๋“ค

// ๋ณด์•ˆ ์ด๋ฒคํŠธ ๋กœ๊น… ์˜ˆ์‹œ
const securityLog = {
  // ์ธ์ฆ ๊ด€๋ จ
  'LOGIN_SUCCESS': { level: 'INFO', fields: ['userId', 'ip', 'userAgent', 'timestamp'] },
  'LOGIN_FAILURE': { level: 'WARN', fields: ['username', 'ip', 'reason', 'timestamp'] },
  'LOGOUT': { level: 'INFO', fields: ['userId', 'ip', 'timestamp'] },
  'MFA_FAILURE': { level: 'WARN', fields: ['userId', 'ip', 'timestamp'] },

  // ์ ‘๊ทผ ์ œ์–ด ๊ด€๋ จ
  'ACCESS_DENIED': { level: 'WARN', fields: ['userId', 'resource', 'ip', 'timestamp'] },
  'PRIVILEGE_ESCALATION_ATTEMPT': { level: 'ERROR', fields: ['userId', 'ip', 'timestamp'] },

  // ๋ฐ์ดํ„ฐ ๊ด€๋ จ
  'SENSITIVE_DATA_ACCESS': { level: 'INFO', fields: ['userId', 'dataType', 'timestamp'] },
  'BULK_DATA_EXPORT': { level: 'WARN', fields: ['userId', 'recordCount', 'timestamp'] },
}

๋กœ๊ทธ์— ์ ˆ๋Œ€ ๊ธฐ๋กํ•˜๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ๋“ค

// โŒ ์ ˆ๋Œ€ ๋กœ๊ทธ์— ๋‚จ๊ธฐ๋ฉด ์•ˆ ๋˜๋Š” ๋ฐ์ดํ„ฐ
logger.info(`Login attempt: username=${username}, password=${password}`)  // ๋น„๋ฐ€๋ฒˆํ˜ธ
logger.info(`Card payment: card=${cardNumber}, cvv=${cvv}`)               // ๊ฒฐ์ œ ์ •๋ณด
logger.info(`Token: ${authToken}`)                                         // ์ธ์ฆ ํ† ํฐ
logger.info(`SSN: ${socialSecurityNumber}`)                                // ๊ฐœ์ธ์ •๋ณด

// โœ… ์•ˆ์ „ํ•œ ๋กœ๊น…
logger.info(`Login attempt: userId=${userId}, ip=${ip}`)
logger.info(`Payment processed: orderId=${orderId}, amount=${amount}`)

๋ฐฉ์–ด ์ „๋žต

  • ์ค‘์•™ํ™”๋œ ๋กœ๊ทธ ์ˆ˜์ง‘ (ELK Stack, Splunk, CloudWatch)
  • ๋กœ๊ทธ ๋ณ€์กฐ ๋ฐฉ์ง€: append-only ์Šคํ† ๋ฆฌ์ง€, ์™ธ๋ถ€ SIEM ์ „์†ก
  • ์ด์ƒ ํ–‰์œ„ ์•Œ๋ฆผ: ๋™์ผ IP์—์„œ ์งง์€ ์‹œ๊ฐ„ ๋‚ด ๋‹ค์ˆ˜ ๋กœ๊ทธ์ธ ์‹คํŒจ
  • ๋กœ๊ทธ ๋ณด๊ด€ ๊ธฐ๊ฐ„: ์ตœ์†Œ 1๋…„ (GDPR/๊ฐœ์ธ์ •๋ณด๋ณดํ˜ธ๋ฒ• ๊ณ ๋ ค)
  • ๋กœ๊ทธ ๋ ˆ๋ฒจ ๊ตฌ๋ถ„: DEBUG๋Š” ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—๋งŒ, ํ”„๋กœ๋•์…˜์€ INFO ์ด์ƒ

A10:2025 โ€” Mishandling of Exceptional Conditions ๐Ÿ†•

๊ฐœ์š”

2025๋…„ ์‹ ์„ค ์นดํ…Œ๊ณ ๋ฆฌ. 2021์˜ SSRF๊ฐ€ ์ œ๊ฑฐ๋˜๊ณ  ์ƒˆ๋กญ๊ฒŒ ๋“ฑ์žฅํ–ˆ๋‹ค.
์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์˜ˆ์™ธ ์ƒํ™ฉ(์˜ค๋ฅ˜, ์—ฃ์ง€ ์ผ€์ด์Šค, ๋ฆฌ์†Œ์Šค ๋ถ€์กฑ) ์„ ์ œ๋Œ€๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ๋‹ค๋ฃฌ๋‹ค.

24๊ฐœ์˜ CWE๊ฐ€ ๋งคํ•‘๋˜์–ด ์žˆ๋‹ค.

๋Œ€ํ‘œ ์‚ฌ๋ก€

โ‘  ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ๋ฆฌ์†Œ์Šค ๋ฏธํ•ด์ œ

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ํŒŒ์ผ ๋ฝ ๋ฏธํ•ด์ œ
public void processUpload(MultipartFile file) throws IOException {
    FileOutputStream fos = new FileOutputStream("temp_" + file.getOriginalFilename());
    fos.write(file.getBytes()); // ์—ฌ๊ธฐ์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ fos๊ฐ€ ๋‹ซํžˆ์ง€ ์•Š์Œ
    // โ†’ ํŒŒ์ผ ์ž ๊ธˆ ์ƒํƒœ ์œ ์ง€, ๋””์Šคํฌ ๊ณ ๊ฐˆ ๊ณต๊ฒฉ ๊ฐ€๋Šฅ
}

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: try-with-resources
public void processUpload(MultipartFile file) throws IOException {
    try (FileOutputStream fos = new FileOutputStream("temp_" + file.getOriginalFilename())) {
        fos.write(file.getBytes());
    } // ์˜ˆ์™ธ ๋ฐœ์ƒํ•ด๋„ ์ž๋™์œผ๋กœ close() ํ˜ธ์ถœ
}

โ‘ก ๊ธˆ์œต ํŠธ๋žœ์žญ์…˜ ๋ถ€๋ถ„ ๋กค๋ฐฑ ์‹คํŒจ

// ์ทจ์•ฝํ•œ ์ฝ”๋“œ: ์ด์ฒด ์ค‘ ์˜ค๋ฅ˜ ์‹œ ๋ถ€๋ถ„๋งŒ ์ฒ˜๋ฆฌ
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    accountRepository.deduct(fromId, amount);   // ์ถœ๊ธˆ ์„ฑ๊ณต
    // ์—ฌ๊ธฐ์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ โ†’
    accountRepository.deposit(toId, amount);    // ์ž…๊ธˆ ๋ˆ„๋ฝ โ†’ ๋ˆ์ด ์‚ฌ๋ผ์ง
}

// ์•ˆ์ „ํ•œ ์ฝ”๋“œ: ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ๊ธฐ
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    accountRepository.deduct(fromId, amount);
    accountRepository.deposit(toId, amount);
    // ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์ „์ฒด ๋กค๋ฐฑ โ†’ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€
}

โ‘ข ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋กœ ์‹œ์Šคํ…œ ์ •๋ณด ๋…ธ์ถœ

# ์ทจ์•ฝํ•œ ์ฝ”๋“œ
try:
    result = db.execute(query)
except Exception as e:
    return {"error": str(e)}
    # ๋ฐ˜ํ™˜ ์˜ˆ์‹œ: "psycopg2.errors.UndefinedTable: relation 'users' does not exist"
    # โ†’ DB ์ข…๋ฅ˜, ํ…Œ์ด๋ธ” ๊ตฌ์กฐ, ๋ฒ„์ „ ์ •๋ณด ๋…ธ์ถœ

# ์•ˆ์ „ํ•œ ์ฝ”๋“œ
try:
    result = db.execute(query)
except Exception as e:
    logger.error(f"DB error: {e}", exc_info=True)  # ๋‚ด๋ถ€ ๋กœ๊ทธ์—๋งŒ
    return {"error": "์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}

๋ฐฉ์–ด ์ „๋žต

  • Fail Closed ์›์น™: ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์•ˆ์ „ํ•œ ์ƒํƒœ๋กœ ์ข…๋ฃŒ (๋ถ€๋ถ„ ์ฒ˜๋ฆฌ ๊ธˆ์ง€)
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์€ ํŠธ๋žœ์žญ์…˜ ์œผ๋กœ ๋ฌถ๊ธฐ
  • ๋ฆฌ์†Œ์Šค๋Š” ๋ฐ˜๋“œ์‹œ finally ๋˜๋Š” try-with-resources ๋กœ ํ•ด์ œ
  • Rate Limiting + ๋ฆฌ์†Œ์Šค ์ฟผํ„ฐ๋กœ ๋ฆฌ์†Œ์Šค ๊ณ ๊ฐˆ ๊ณต๊ฒฉ ๋ฐฉ์–ด
  • ์ค‘์•™ํ™”๋œ ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„ (์—๋Ÿฌ๋ณ„ ์ฒ˜๋ฆฌ ์ผ๊ด€์„ฑ)

์ „์ฒด ์œ„ํ—˜๋„ ์š”์•ฝ

๋“ฑ๊ธ‰ ํ•ญ๋ชฉ ์ด๋ฆ„ 2021 ๋Œ€๋น„ ํ•ต์‹ฌ ์œ„ํ—˜
๐Ÿ”ด Critical A01 Broken Access Control ์œ ์ง€ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์ ‘๊ทผ
๐Ÿ”ด Critical A02 Security Misconfiguration โฌ†๏ธ A05โ†’A02 ์„ค์ • ์˜ค๋ฅ˜๋กœ ์‹œ์Šคํ…œ ๋…ธ์ถœ
๐Ÿ”ด Critical A03 Supply Chain Failures ๐Ÿ†• ๋ฒ”์œ„ ํ™•๋Œ€ ๋นŒ๋“œ/๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ ์นจํ•ด
๐ŸŸ  High A04 Cryptographic Failures โฌ‡๏ธ A02โ†’A04 ๋ฐ์ดํ„ฐ ํ‰๋ฌธ ๋…ธ์ถœ
๐ŸŸ  High A05 Injection โฌ‡๏ธ A03โ†’A05 ์•…์˜์  ์ฟผ๋ฆฌ/๋ช…๋ น ์‹คํ–‰
๐ŸŸ  High A06 Insecure Design โฌ‡๏ธ A04โ†’A06 ์•„ํ‚คํ…์ฒ˜ ์ˆ˜์ค€ ์ทจ์•ฝ์ 
๐ŸŸก Medium A07 Auth Failures ์œ ์ง€ ๊ณ„์ • ํƒˆ์ทจ
๐ŸŸก Medium A08 Integrity Failures ์œ ์ง€ ์œ„์กฐ ์ฝ”๋“œ/๋ฐ์ดํ„ฐ ์‹คํ–‰
๐ŸŸก Medium A09 Logging Failures ์œ ์ง€ ์นจํ•ด ํƒ์ง€ ๋ถˆ๊ฐ€
๐ŸŸก Medium A10 Exceptional Conditions ๐Ÿ†• SSRF ๋Œ€์ฒด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์‹คํŒจ

์‹ค๋ฌด ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๊ฐœ๋ฐœ ์™„๋ฃŒ ์ „ ์•„๋ž˜ ํ•ญ๋ชฉ์„ ์ ๊ฒ€ํ•ด๋ณด์ž.

์ ‘๊ทผ ์ œ์–ด

  • ๋ชจ๋“  API ์—”๋“œํฌ์ธํŠธ์— ์ธ์ฆ/์ธ๊ฐ€ ์ ์šฉ ํ™•์ธ
  • IDOR ๊ฐ€๋Šฅ์„ฑ โ€” URL์˜ ID ํŒŒ๋ผ๋ฏธํ„ฐ ์†Œ์œ ๊ถŒ ๊ฒ€์ฆ
  • ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ์ ‘๊ทผ ๋ถˆ๊ฐ€ ํ™•์ธ
  • JWT ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ช…์‹œ์  ๊ณ ์ •, ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ

์„ค์ • ๋ณด์•ˆ

  • ๊ธฐ๋ณธ ๊ณ„์ •/๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ
  • ๋ถˆํ•„์š”ํ•œ ํฌํŠธ/์„œ๋น„์Šค ๋น„ํ™œ์„ฑํ™”
  • HTTP ๋ณด์•ˆ ํ—ค๋” ์„ค์ • (securityheaders.com ์—์„œ ์ ๊ฒ€ ๊ฐ€๋Šฅ)
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์‚ฌ์šฉ์ž ๋…ธ์ถœ ์—ฌ๋ถ€ ํ™•์ธ
  • ํ”„๋กœ๋•์…˜์—์„œ ๋””๋ฒ„๊ทธ ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™”

๊ณต๊ธ‰๋ง ๋ณด์•ˆ

  • ์˜์กด์„ฑ ๋ฒ„์ „ ๊ณ ์ • (lock ํŒŒ์ผ ์ปค๋ฐ‹)
  • npm audit / pip-audit / trivy ์ •๊ธฐ ์‹คํ–‰
  • CI/CD ์™ธ๋ถ€ ์•ก์…˜ ์ปค๋ฐ‹ ํ•ด์‹œ ๊ณ ์ •
  • SBOM ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ

์•”ํ˜ธํ™”

  • ๋น„๋ฐ€๋ฒˆํ˜ธ bcrypt/Argon2๋กœ ์ €์žฅ
  • ์ „์†ก ๊ตฌ๊ฐ„ HTTPS ๊ฐ•์ œ (HSTS ํ—ค๋”)
  • ์•”ํ˜ธํ™” ํ‚ค ์ฝ”๋“œ์—์„œ ๋ถ„๋ฆฌ (ํ™˜๊ฒฝ๋ณ€์ˆ˜/Secrets Manager)
  • TLS 1.2 ์ด์ƒ๋งŒ ํ—ˆ์šฉ

๋กœ๊น…

  • ๋กœ๊ทธ์ธ ์„ฑ๊ณต/์‹คํŒจ ๊ธฐ๋ก
  • ์ ‘๊ทผ ๊ฑฐ๋ถ€ ์ด๋ฒคํŠธ ๊ธฐ๋ก
  • ๋กœ๊ทธ์— ๋น„๋ฐ€๋ฒˆํ˜ธ/ํ† ํฐ/์นด๋“œ๋ฒˆํ˜ธ ํฌํ•จ ์—ฌ๋ถ€ ํ™•์ธ
  • ์ด์ƒ ํ–‰์œ„ ์•Œ๋ฆผ ์„ค์ •

๋งˆ์น˜๋ฉฐ

OWASP Top 10์€ ์ตœ์†Œํ•œ์˜ ๊ธฐ์ค€์ด๋‹ค.
์ด 10๊ฐ€์ง€๋ฅผ ๋ชจ๋‘ ์žก์•„๋„ ๋ณด์•ˆ์ด ์™„๋ฒฝํ•˜๋‹ค๋Š” ์˜๋ฏธ๋Š” ์•„๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ์ด๊ฒƒ์กฐ์ฐจ ๋†“์น˜๊ณ  ์žˆ๋‹ค๋ฉด, ๊ณต๊ฒฉ์ž ์ž…์žฅ์—์„  ๊ทธ๋ƒฅ ๊ณต๊ฐœ๋œ ๋ฌธ์„ ์—ฌ๋Š” ๊ฒƒ๊ณผ ๊ฐ™๋‹ค.

๋ณด์•ˆ์€ ํ•œ ๋ฒˆ ์ ์šฉํ•˜๊ณ  ๋๋‚˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋‹ค.
์œ„ํ˜‘ ํ™˜๊ฒฝ์€ ๊ณ„์† ๋ณ€ํ•˜๊ณ , OWASP๋„ 4๋…„๋งˆ๋‹ค ๋ฐ”๋€๋‹ค.
์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋•Œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ•œ ์ค„์”ฉ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด๋ณด์ž.


๊ด€๋ จ ๊ธ€


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