07. OWASP Top 10 (2021)
07. OWASP Top 10 (2021)¶
ìŽì : 06. ìžê°ì ì ê·Œ ì ìŽ | ë€ì: 08. ìžì ì 공격곌 ë°©ìŽ
OWASP (Open Worldwide Application Security Project) Top 10ì ì¹ ì í늬ìŒìŽì 볎ì ìíì ëíŽ ê°ì¥ ë늬 ìžì ë°ë 묞ìì ëë€. ì€ì ì·šìœì ë°ìŽí°ë¥Œ êž°ë°ìŒë¡ 죌Ʞì ìŒë¡ ì ë°ìŽížëë©°, ê°ë°ìì 볎ì ì 묞ê°ë¥Œ ìí íì€ ìžì 묞ì ìí ì í©ëë€. 2021ë íì ìí í겜ì ì€ëí ë³í륌 ë°ìíì¬ ìž ê°ì§ ìë¡ìŽ ì¹Ží ê³ ëŠ¬ì 죌ì ì¬ížì±ìŽ ììµëë€. ìŽ ë ìšììë ì€ëª , ì€ì ì¬ë¡, ì·šìœí ìœë, ìì ë ìœë, ë°©ìŽ ì ëµê³Œ íšê» ìŽ ê°ì§ 칎í ê³ ëŠ¬ ê°ê°ì ë€ë£¹ëë€.
íìµ ëª©í¶
- OWASP Top 10 (2021) 몚ë 칎í ê³ ëŠ¬ ìŽíŽíêž°
- ê° ì¹Ží ê³ ëŠ¬ì ì·šìœí ìœë íšíŽ ìë³íêž°
- ê° ì·šìœì íŽëì€ë¥Œ ë°©ì§íë ìì í ìœë ìì±íêž°
- ê°ë° ë° ìœë 늬뷰 ì 볎ì 첎í¬ëЬì€ížë¡ OWASP Top 10 ì ì©íêž°
- ê° ì·šìœì ì ì€ì ìí¥ ìžìíêž°
1. ê°ì: 2021 Top 10¶
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â OWASP Top 10 - 2021 â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â # 칎í
ê³ ëŠ¬ 2017ë
ëë¹ ë³í â
â âââ ââââââââââââââââââââââââââââââââââââ ââââââââââââââââââââ â
â A01 ì·šìœí ì ê·Œ ì ìŽ â #5ìì ìì¹ â
â A02 ìíží ì€íš â #3ìì ìì¹(ìŽëŠë³ê²œ)â
â A03 ìžì ì
â #1ìì íëœ â
â A04 ìì íì§ ìì ì€ê³ â
ì ê· â
â A05 볎ì êµ¬ì± ì€ë¥ â #6ìì ìì¹ â
â A06 ì·šìœíê³ ì€ëë êµ¬ì± ìì â #9ìì ìì¹(ìŽëŠë³ê²œ)â
â A07 ìë³ ë° ìžìŠ ì€íš â #2ìì íëœ(ìŽëŠë³ê²œ)â
â A08 ìíížìšìŽ ë° ë°ìŽí° ë¬Žê²°ì± ì€íš â
ì ê· â
â A09 볎ì ë¡ê¹
ë° ëªšëí°ë§ ì€íš â #10ìì ìì¹(ìŽëŠë³ê²œ)â
â A10 ìë² ìž¡ ìì² ìì¡° (SSRF) â
ì ê· â
â â
â 죌ì ížë ë: â
â - ì·šìœí ì ê·Œ ì ìŽê° 1ì (í
ì€ížë ì±ì 94%) â
â - ìžì ì
ìŽ #1ìì #3ìŒë¡ íëœ (íë ììí¬ê° ëì) â
â - ìž ê°ì§ ì 칎í
ê³ ëŠ¬ê° íë ìí ë°ì â
â - ê³µêžë§ ë° ë¬Žê²°ì± ì°ë € ìŠê° â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
2. A01: ì·šìœí ì ê·Œ ì 쎶
2.1 ì€ëª ¶
ì ê·Œ ì ìŽë ì¬ì©ìê° ìëë ê¶íì ë²ìŽë íëí ì ìëë¡ ì ì± ì ê°ì í©ëë€. ì·šìœí ì ê·Œ ì ìŽë ê°ì¥ ìŒë°ì ìž ì¹ ì í늬ìŒìŽì ì·šìœì ìŒë¡, í ì€ížë ì í늬ìŒìŽì ì 94%ìì ë°ê²¬ë©ëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A01: ì·šìœí ì ê·Œ ì ìŽ â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â ìŒë°ì ìž ìœì : â
â - URL/íëŒë¯ží°/API ìì ìŒë¡ ì ê·Œ ì ìŽ ì°í â
â - ë€ë¥ž ì¬ëì ê³ì 볎Ʞ ëë ížì§ (IDOR) â
â - ì ê·Œ ì ìŽê° ëëœë API ì ê·Œ (POST/PUT/DELETE) â
â - ê¶í ìì¹ (ë¡ê·žìž ììŽ êŽëЬìë¡ íë) â
â - ë©íë°ìŽí° ì¡°ì (JWT ì¬ì, ì¿ í€ ë³ì¡°) â
â - ë¬Žëš API ì ê·Œì íì©íë CORS êµ¬ì± ì€ë¥ â
â - ìžìŠëì§ ìì/êŽëЬì íìŽì§ë¡ ê°ì ëžëŒì°ì§ â
â â
â ìí¥: ë°ìŽí° ëë, ë¬Žëš ë°ìŽí° ìì , â
â ê³ì íì·š, ì 첎 ìì€í
ì¹šíŽ â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
2.2 ì·šìœí ìœë¶
# ì·šìœ: êŽëЬì ìëí¬ìžížì ì ê·Œ ì ìŽ ìì
@app.route('/admin/users/<int:user_id>/delete', methods=['POST'])
def delete_user(user_id):
# ìŽ URLì ìë ì¬ëì ë구ë ì¬ì©ì륌 ìì í ì ìì!
db.delete_user(user_id)
return jsonify({"status": "deleted"})
# ì·šìœ: IDOR - ìì ê¶ íìž ìì
@app.route('/api/orders/<int:order_id>')
@require_auth
def get_order(order_id):
order = db.get_order(order_id)
return jsonify(order) # ì¬ì©ì Aê° ì¬ì©ì Bì 죌묞ì 볌 ì ìì
2.3 ìì ë ìœë¶
# ìì : ì ì í ìžê° íìž
@app.route('/admin/users/<int:user_id>/delete', methods=['POST'])
@require_role('admin') # êŽëЬìë§ ì ê·Œ ê°ë¥
def delete_user(user_id):
db.delete_user(user_id)
return jsonify({"status": "deleted"})
# ìì : ìì ê¶ ê²ìŠ
@app.route('/api/orders/<int:order_id>')
@require_auth
def get_order(order_id):
order = db.get_order(order_id)
if not order:
return jsonify({"error": "Not found"}), 404
# ì£Œë¬žìŽ ìžìŠë ì¬ì©ììê² ìíëì§ ê²ìŠ
if order['user_id'] != g.current_user['id']:
return jsonify({"error": "Not found"}), 404 # 403ìŽ ìë 404
return jsonify(order)
2.4 방쎶
- ê³µê° ëŠ¬ìì€ë¥Œ ì ìžíê³ êž°ë³žì ìŒë¡ ê±°ë¶
- ì ê·Œ ì ìŽ ë©ì»€ëìŠì í ë² êµ¬ííê³ ëªšë ê³³ìì ì¬ì¬ì©
- ë ìœë ìì ê¶ ê°ì (ì¬ì©ìê° ì ê³µí IDìë§ ì졎íì§ ìêž°)
- ì¹ ìë² ëë í 늬 ëª©ë¡ ë¹íì±í
- ì ê·Œ ì ìŽ ì€íšë¥Œ ë¡ê·žì êž°ë¡íê³ êŽëЬììê² ì늌
- ìëíë ì€ìº íŒíŽë¥Œ ìµìííêž° ìíŽ API ì ê·Œ ìë ì í
- ë¡ê·žìì ì JWT í í° ë¬Žíší (ìë² ìž¡ í í° ì°šëš ëª©ë¡)
3. A02: ìíží ì€íš¶
3.1 ì€ëª ¶
ìŽì ìë "믌ê°í ë°ìŽí° ë žì¶"ìŽëŒê³ ë¶ë žìŒë©°, ìŽ ì¹Ží ê³ ëŠ¬ë 믌ê°í ë°ìŽí° ë žì¶ë¡ ìŽìŽì§ë ìíží êŽë š ì€íšì ìŽì ì ë§ì¶¥ëë€. 볎ížíŽìŒ í ë°ìŽí°ë¥Œ ìížííì§ ìë ê²ê³Œ ìœíê±°ë ì€ëë ìíží ìê³ ëŠ¬ìŠ ì¬ì©ì 몚ë í¬íší©ëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A02: ìíží ì€íš â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â ì€ì€ë¡ìê² ë¬ŒìŽë³Žìžì: â
â 1. ìŽë€ ë°ìŽí°ê° í묞ìŒë¡ ì ì¡ëê±°ë ì ì¥ëëê°? â
â 2. ì€ëëê±°ë ìœí ìíží ìê³ ëŠ¬ìŠìŽë íë¡í ìœìŽ ì¬ì©ëëê°? â
â 3. Ʞ볞 ìíží í€ê° ì¬ì©ëê±°ë í€ê° ìíëì§ ìëê°? â
â 4. ìížíê° ê°ì ëì§ ìëê° (HTTPS 늬ëë ì
ëëœ)? â
â 5. ì ì í HTTP 볎ì í€ëê° ëëœëìŽ ìëê°? â
â 6. ìë² ìžìŠìê° ì¬ë°ë¥Žê² ê²ìŠëëê°? â
â 7. ë¹ë°ë²ížì ëíŽ deprecated íŽì±ìŽ ì¬ì©ëëê° (MD5, SHA1)? â
â â
â 믌ê°í ë°ìŽí° 칎í
ê³ ëŠ¬: â
â - ë¹ë°ë²íž, ì ì©ì¹Žë ë²íž, ê±Žê° êž°ë¡ â
â - ê°ìž ë°ìŽí°, ë¹ìŠëì€ ë¹ë° â
â - ê°ìžì 볎 ë³Žíž ê·ì ì ìíŽ ë³Žížëë ë°ìŽí° (GDPR, HIPAA, PCI)â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
3.2 ì·šìœí ìœë¶
# ì·šìœ: MD5ë¡ ë¹ë°ë²íž ì ì¥
import hashlib
def store_password(password):
# MD5ë ë¹ë°ë²íž íŽì±ì 깚ì¡ì!
hash_value = hashlib.md5(password.encode()).hexdigest()
db.store(hash_value)
# ì·šìœ: íëìœë©ë ìíží í€
ENCRYPTION_KEY = "my-secret-key-123" # ìì€ ì ìŽì 컀ë°ëš!
# ì·šìœ: ECB 몚ë ì¬ì© (íšíŽ ëë¬ëš)
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_ECB) # ECB 몚ëë ìì íì§ ìì!
encrypted = cipher.encrypt(data)
# ì·šìœ: 믌ê°í ë°ìŽí°ì HTTP ì¬ì©
# HTTPìì HTTPSë¡ì 늬ëë ì
ìì
# HSTS í€ë ìì
3.3 ìì ë ìœë¶
# ìì : ë¹ë°ë²ížì Argon2 ì¬ì©
from argon2 import PasswordHasher
ph = PasswordHasher()
def store_password(password):
hash_value = ph.hash(password) # ìë ìí
ì ì¬ì©í Argon2id
db.store(hash_value)
# ìì : í겜ìì í€ ê°ì žì€êž°, ìì€ ìœëìì ê°ì žì€ì§ ìêž°
import os
ENCRYPTION_KEY = os.environ['ENCRYPTION_KEY'] # 256ë¹íž í€
# ìì : GCM 몚ë ì¬ì© (ìžìŠë ìíží)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12) # nonce륌 ì ë ì¬ì¬ì©íì§ ë§ìžì
encrypted = aesgcm.encrypt(nonce, data, associated_data)
# ìì : HTTPS ê°ì ë° ë³Žì í€ë ì¶ê°
@app.after_request
def set_security_headers(response):
response.headers['Strict-Transport-Security'] = \
'max-age=31536000; includeSubDomains'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
return response
3.4 방쎶
- 믌ê°ëì ë°ëŒ ë°ìŽí° ë¶ë¥; ë¶ë¥ë³ë¡ ì ìŽ ì ì©
- ë¶íìí 믌ê°í ë°ìŽí°ë¥Œ ì ì¥íì§ ë§ìžì; ê°ë¥í í 빚늬 íêž°
- 몚ë 믌ê°í ë°ìŽí°ë¥Œ ì ì¥ ì ë° ì ì¡ ì ìíží (TLS 1.2+)
- ê°ë ¥íê³ íì€ ìê³ ëŠ¬ìŠ ì¬ì© (AES-256-GCM, RSA-2048+, Ed25519)
- ìžìŠë ìíží ì¬ì© (GCM, ChaCha20-Poly1305), ECBë ì ë ì¬ì© ì íš
- Argon2id, bcrypt ëë scrypt륌 ì¬ì©íì¬ ë¹ë°ë²íž ì ì¥
- ìížíì ìŒë¡ ìì í ëì ìì±êž°ë¥Œ ì¬ì©íì¬ í€ ìì±
- 믌ê°í ë°ìŽí°ê° í¬íšë íìŽì§ì ìºì± ë¹íì±í
4. A03: ìžì ì ¶
4.1 ì€ëª ¶
ìžì ì ê²°íšì ì 뢰í ì ìë ë°ìŽí°ê° ëª ë ¹ ëë 쿌늬ì ìŒë¶ë¡ ìží°í늬í°ì ì ì¡ë ë ë°ìí©ëë€. 공격ìì ì ìì ìž ë°ìŽí°ë ìží°í늬í°ë¥Œ ìì¬ ìëíì§ ìì ëª ë ¹ì ì€ííê±°ë ê¶í ììŽ ë°ìŽí°ì ì ê·Œíê² í ì ììµëë€. SQL ìžì ì , NoSQL ìžì ì , OS ëª ë ¹ ìžì ì , LDAP ìžì ì ì 몚ë ìŽ ì·šìœì ì ííì ëë€.
ì°žê³ : ìžì ì ì ë ìš 08ìì íšì¬ ë ììží ë€ë£¹ëë€. ìŽ ì¹ì ì ììœì ì ê³µí©ëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A03: ìžì ì
â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â ìžì ì
ì í: â
â âââ SQL ìžì ì
(ê°ì¥ ìŒë°ì ) â
â âââ NoSQL ìžì ì
(MongoDB ë±) â
â âââ ëª
ë ¹ ìžì ì
(os.system, subprocess) â
â âââ LDAP ìžì ì
(ëë í 늬 ìë¹ì€) â
â âââ XPath ìžì ì
(XML 쿌늬) â
â âââ í
í늿 ìžì ì
(Jinja2, Twig SSTI) â
â âââ íí ìžìŽ (Spring EL, OGNL) â
â â
â 귌볞 ììž: â
â ì¬ì©ì ì
ë ¥ìŽ ë§€ê°ë³ìíëê±°ë ì ì í ìŽì€ìŒìŽíëë ëì â
â 쿌늬/ëª
ë ¹ì ì°ê²°ëš â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
4.2 ì·šìœ ìœë vs ìì ë ìœë¶
# ì·šìœ: SQL ìžì ì
@app.route('/search')
def search():
q = request.args.get('q')
# 묞ììŽ ì°ê²° = SQL ìžì ì
!
results = db.execute(f"SELECT * FROM products WHERE name LIKE '%{q}%'")
return jsonify(results)
# 공격: /search?q=' OR '1'='1' --
# ìì : ë§€ê°ë³ìíë 쿌늬
@app.route('/search')
def search():
query = request.args.get('q', '')
results = db.execute(
"SELECT * FROM products WHERE name LIKE :query",
{"query": f"%{query}%"} # íëŒë¯ží° ë°ìžë©
)
return jsonify(results)
4.3 방쎶
- ë§€ê°ë³ìíë 쿌늬 / ì€ë¹ë 묞 ì¬ì© (íì)
- ë§€ê°ë³ìí륌 ì²ëЬíë ORM íë ììí¬ ì¬ì© (SQLAlchemy, Django ORM)
- 몚ë ì ë ¥ ê²ìŠ ë° ì ì (íìŽížëЬì€íž ê²ìŠ)
- í¹ì ìží°í늬í°ì ëí í¹ì 묞ì ìŽì€ìŒìŽí
- ìžì ì ì ëë ê³µê°ë¥Œ ë°©ì§íêž° ìíŽ ì¿ŒëŠ¬ì LIMIT ì¬ì©
5. A04: ìì íì§ ìì ì€ê³¶
5.1 ì€ëª ¶
ìŽê²ì 2021ë ì ìë¡ìŽ ì¹Ží ê³ ëŠ¬ë¡ ì€ê³ ë° ìí€í ì² ê²°íšê³Œ êŽë šë ìíì ìŽì ì ë§ì¶¥ëë€. ìí 몚ëžë§, 볎ì ì€ê³ íšíŽ ë° ì°žì¡° ìí€í ì²ì ë ë§ì ì¬ì©ì ì구í©ëë€. ìì íì§ ìì ì€ê³ë ì벜í 구íìŒë¡ë ìì í ì ììµëë€ - ê²°íšìŽ ìë ì€ê³ë 볞ì§ì ìŒë¡ ì·šìœí©ëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A04: ìì íì§ ìì ì€ê³ â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â ìì íì§ ìì ì€ê³ â ìì íì§ ìì 구í â
â â
â ââââââââââââââââââââ ââââââââââââââââââââ â
â â ìì íì§ ìì â â ìì íì§ ìì â â
â â ì€ê³ â â 구í â â
â â â â â â
â â ì²ì¬ì§ìŽ ê²°íšìŽ â â ì²ì¬ì§ì ì¢ì§ë§ â â
â â ìì. ìœë ìì â â ìœëì ë²ê·žê° â â
â â ìŒë¡ ëì ìëš. â â ìì. â â
â â â â â â
â â ìì: â â ìì: â â
â â ë¹ë°ë²íž ì¬ì€ì â â ë¡ê·žìž íŒì â â
â â ìŽë©ìŒë¡ í묞 â â SQL ìžì ì
â â
â â ë¹ë°ë²íž ì ì¡ â â â â
â â (ì€ê³ì) â â â â
â ââââââââââââââââââââ ââââââââââââââââââââ â
â â
â ìì íì§ ìì ì€ê³ì ì: â
â - ìžìŠì ìë ì í ìì (ë¬Žì°šë³ ëì
공격 ê°ë¥) â
â - ë¹ë°ë²íž 복구ì ì ìŒí ë°©ë²ìŒë¡ 볎ì ì§ë¬ž ì¬ì© â
â - ì
ë ¥ ê²ìŠ ìí€í
ì² ìì (ê° ê°ë°ìê° ìì ì ë°©ììŒë¡ 구í) â
â - ì€ê³ì ìì€ ìœëì ë¹ë° ì ì¥ â
â - ë€ì€ í
ëíž ìì€í
ìì í
ëíž ë°ìŽí° ê° ë¶ëЬ ìì â
â - ì구ì¬íìì ëšì© ì¬ë¡ ëëœ (íë³µí 겜ë¡ë§) â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
5.2 ì€ì ì¬ë¡: ìíêŽ ì윶
# ìì íì§ ìì ì€ê³: ìë ì í ìë ìí í°ìŒ ììœ
# ì€ê³ì ëŽìŽ ìžêž° ìíì 몚ë í°ìŒì ììœí ì ìì
@app.route('/api/book', methods=['POST'])
@require_auth
def book_ticket():
movie_id = request.json['movie_id']
seats = request.json['seats'] # ì¢ì ì ì í ìì!
# ì¬ì©ìë¹ ìë ì í ìì
# ê±°ëë¹ ìµë ì¢ì ì ìì
# ììê° ë§ì ìŽë²€ížì CAPTCHA ìì
# ì¬êž° íì§ ìì
booking = create_booking(g.user.id, movie_id, seats)
return jsonify(booking)
# ìì í ì€ê³: ì ì í ë³Žíž ì¥ì¹ í¬íš
@app.route('/api/book', methods=['POST'])
@require_auth
@rate_limit(max_requests=5, per_minutes=1) # ìë ì í
def book_ticket():
movie_id = request.json['movie_id']
seats = request.json['seats']
# ì€ê³ ìì€ ì ìŽ
MAX_SEATS_PER_BOOKING = 6
if len(seats) > MAX_SEATS_PER_BOOKING:
return jsonify({"error": f"ììœë¹ ìµë {MAX_SEATS_PER_BOOKING}ì"}), 400
# ì¬ì©ìê° ìŽ ììíì ë묎 ë§ì ì¢ìì ìŽë¯ž ììœíëì§ íìž
existing = get_user_bookings(g.user.id, movie_id)
if len(existing) >= 2: # ìíë¹ ì¬ì©ìë¹ ìµë 2ê° ììœ
return jsonify({"error": "ìŽ ìíì ëí ìµë ììœ ìì ëë¬íìµëë€"}), 400
# ììê° ë§ì ìŽë²€ížì ê²œì° ì¶ê° ê²ìŠ íì
movie = get_movie(movie_id)
if movie.get('high_demand'):
if not verify_captcha(request.json.get('captcha_token')):
return jsonify({"error": "CAPTCHA ê²ìŠ íì"}), 400
booking = create_booking(g.user.id, movie_id, seats)
return jsonify(booking)
5.3 방쎶
- ì€ìí ìžìŠ, ì ê·Œ ì ìŽ ë° ë¹ìŠëì€ ë¡ì§ì ìí 몚ëžë§ ì¬ì©
- ì¬ì©ì ì€í 늬ì 볎ì ìžìŽ ë° ì ìŽ íµí©
- 몚ë ì€ìí íëŠìŽ ëšì©ì ì ííëì§ ê²ìŠíë ëšì ë° íµí© í ì€íž ìì±
- ì€íšë¥Œ ìí ì€ê³: ì¬ì©ì/ìžì ë¹ ëŠ¬ìì€ ìë¹ ì í
- ì 뢰 겜ê³ë¥Œ ë¶ëЬíêž° ìíŽ ì í늬ìŒìŽì ë ìŽìŽ ê³ìžµí
- 볎ì ì€ê³ íšíŽ ì¬ì© (ì: 묎ìí ìžì êŽëЬ, ì ë ¥ ê²ìŠ íë ììí¬)
6. A05: 볎ì êµ¬ì± ì€ë¥¶
6.1 ì€ëª ¶
볎ì êµ¬ì± ì€ë¥ë ì€ì ë¡ ê°ì¥ íí 볌 ì ìë 묞ì ì ëë€. ì못 구ì±ë ê¶í, ë¶íìí êž°ë¥ íì±í, Ʞ볞 ê³ì /ë¹ë°ë²íž 믞ë³ê²œ, ì§ëì¹ê² ììží ì€ë¥ ë©ìì§, 볎ì ê°í ëëœìŽ í¬íšë©ëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A05: 볎ì êµ¬ì± ì€ë¥ â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â ìŒë°ì ìž êµ¬ì± ì€ë¥: â
â ââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â 묞ì â ìí â â
â ââââââââââââââââââââââââââââŒâââââââââââââââââââââââ†â
â â íë¡ëì
ìì ëë²ê·ž 몚ë â ì€í ì¶ì ë
žì¶ â â
â â Ʞ볞 admin:admin â ìŠì ì¹šíŽ â â
â â ëë í 늬 ëª©ë¡ íì±í â ìì€/êµ¬ì± ë
žì¶ â â
â â ë¶íìí ìë¹ì€ â 공격 í멎 ìŠê° â â
â â ììží ì€ë¥ ë©ìì§ â ì 볎 ê³µê° â â
â â 볎ì í€ë ëëœ â XSS, íŽëŠì¬í¹ â â
â â CORS: Access-Control- â êµì°š ì¶ì² 공격 â â
â â Allow-Origin: * â â â
â â ì€ëë ìíížìšìŽ â ìë €ì§ ì·šìœì â â
â â S3 ë²í· ê³µê° â ë°ìŽí° ì¹šíŽ â â
â â .git íŽë ë
žì¶ â ìì€ ìœë ì ì¶ â â
â ââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
6.2 ì·šìœí 구챶
# ì·šìœ: íë¡ëì
ìì Flask ëë²ê·ž 몚ë
app = Flask(__name__)
app.config['DEBUG'] = True # ëíí ëë²ê±° ë
žì¶!
app.config['SECRET_KEY'] = 'dev' # ìœí/Ʞ볞 ë¹ë° í€
# ì·šìœ: ì§ëì¹ê² íì©ì ìž CORS
from flask_cors import CORS
CORS(app, origins="*") # 몚ë ì¹ì¬ìŽížê° ìì²í ì ìì!
# ì·šìœ: ììží ì€ë¥ ë©ìì§
@app.errorhandler(500)
def handle_error(error):
return jsonify({
"error": str(error),
"traceback": traceback.format_exc(), # ëŽë¶ ì 볎 ì ì¶!
"database": app.config['DATABASE_URI'], # ì격 ìŠëª
ì ì¶!
}), 500
6.3 ìì ë 구챶
import os
app = Flask(__name__)
# ìì : í겜 êž°ë° êµ¬ì±
app.config['DEBUG'] = False
app.config['TESTING'] = False
app.config['SECRET_KEY'] = os.environ['SECRET_KEY'] # ê°ë ¥íê³ ë¬Žìì
# ìì : ì íì ìž CORS
from flask_cors import CORS
CORS(app, origins=[
"https://myapp.com",
"https://www.myapp.com",
])
# ìì : íë¡ëì
ìì ìŒë°ì ìž ì€ë¥ ë©ìì§
@app.errorhandler(500)
def handle_error(error):
# ì 첎 ì€ë¥ë¥Œ ëŽë¶ì ìŒë¡ ë¡ê·ž
app.logger.error(f"Internal error: {error}", exc_info=True)
# ì¬ì©ììê² ìŒë° ë©ìì§ ë°í
return jsonify({
"error": "ëŽë¶ ì€ë¥ê° ë°ìíìµëë€",
"reference": generate_error_reference_id(), # ì§ì í°ìŒì©
}), 500
# ìì : 볎ì í€ë
@app.after_request
def add_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '0' # ë ê±°ì XSS íí° ë¹íì±í
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
response.headers['Content-Security-Policy'] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
"font-src 'self'; "
"frame-ancestors 'none'"
)
response.headers['Permissions-Policy'] = (
'camera=(), microphone=(), geolocation=()'
)
if request.is_secure:
response.headers['Strict-Transport-Security'] = (
'max-age=31536000; includeSubDomains; preload'
)
return response
6.4 방쎶
- 몚ë í겜ì ëíŽ ë°ë³µ ê°ë¥í ê°í íë¡ìžì€ 구í
- ë¶íìí êž°ë¥, íë ììí¬ ë° êµ¬ì± ìì ì ê±° ëë ì€ì¹íì§ ìêž°
- íšì¹ êŽëЬ íë¡ìžì€ì ìŒë¶ë¡ êµ¬ì± ê²í ë° ì ë°ìŽíž
- ìŒêŽëê³ ê°ì¬ ê°ë¥í ë°°í¬ë¥Œ ìíŽ ìžíëŒë¥Œ ìœëë¡ ì¬ì©
- CI/CDìì ìëíë 볎ì êµ¬ì± ê²ìŠ êµ¬í
- ë€ë¥ž ì격 ìŠëª ìŒë¡ í겜 ë¶ëЬ (ê°ë°, ì€í ìŽì§, íë¡ëì )
7. A06: ì·šìœíê³ ì€ëë êµ¬ì± ìì¶
7.1 ì€ëª ¶
ìë €ì§ ì·šìœì ìŽ ìë êµ¬ì± ìì (ëŒìŽëžë¬ëЬ, íë ììí¬, OS)륌 ì¬ì©íë ì í늬ìŒìŽì ì ì ì©ë ì ììµëë€. ìŽê²ì ì ì ë ì€ìíŽì§ë ê³µêžë§ ìíì ëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A06: ì·šìœíê³ ì€ëë êµ¬ì± ìì â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â ë€ì곌 ê°ì ê²œì° ì·šìœí©ëë€: â
â - ì¬ì©ë 몚ë êµ¬ì± ììì ë²ì ì ëªšëŠ â
â - ìíížìšìŽê° ì§ìëì§ ìê±°ë íšì¹ëì§ ìì â
â - ì êž°ì ìŒë¡ ì·šìœì ì ì€ìºíì§ ìì â
â - ì ìì ìì íê±°ë ì
ê·žë ìŽëíì§ ìì â
â - ê°ë°ìê° ì
ë°ìŽížë ëŒìŽëžë¬ëЬì ížíì±ì í
ì€ížíì§ ìì â
â - êµ¬ì± ìì 구ì±ìŽ ë³Žìëì§ ìì (A05 ì°žì¡°) â
â â
â ì€ì ìí¥: â
â - Log4Shell (CVE-2021-44228): Log4jì ì¹ëª
ì RCE â
â - Equifax ì¹šíŽ (2017): íšì¹ëì§ ìì Apache Struts â
â - Event-Stream (2018): ì
ìì ìž npm íší€ì§ â
â - ua-parser-js (2021): npmì ê³µêžë§ 공격 â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
7.2 ì¢ ìì± ê°ì¬¶
# Python: ìë €ì§ ì·šìœì íìž
pip install pip-audit
pip-audit # ì€ì¹ë íší€ì§ ì€ìº
pip-audit -r requirements.txt # requirements íìŒ ì€ìº
# Python: safetyë¡ ë첎
pip install safety
safety check # ì€ì¹ë íší€ì§ íìž
# Node.js: ëŽì¥ ê°ì¬
npm audit
npm audit fix # ê°ë¥í ê²œì° ìë ìì
# ìŒë°: OWASP Dependency-Check
# ì¬ë¬ ìžìŽ ì€ìº (Java, .NET, Python, JS ë±)
# https://owasp.org/www-project-dependency-check/
# GitHub: Dependabot (ì·šìœí ì¢
ìì±ì ëí ìë PR)
# GitLab: CI/CD íìŽíëŒìžì ì¢
ìì± ì€ìº
# íŽì ê²ìŠìŒë¡ ì¢
ìì± ê³ ì (Python)
pip install --require-hashes -r requirements.txt
# ê³ ì ë ë²ì 곌 íŽìê° ìë requirements.txt
# ìì±: pip-compile --generate-hashes requirements.in
flask==3.0.0 \
--hash=sha256:21128f47e...
werkzeug==3.0.1 \
--hash=sha256:5a7b12abc...
7.3 방쎶
- ì¬ì©íì§ ìë ì¢ ìì±, êž°ë¥, êµ¬ì± ìì, íìŒ ë° ë¬žì ì ê±°
- ë구륌 ì¬ì©íì¬ êµ¬ì± ìì ë²ì ì ì§ìì ìŒë¡ 목ë¡í (pip-audit, npm audit, OWASP Dependency-Check)
- ì·šìœì ì늌ì ìíŽ CVE ë° NVDì ê°ì ìì€ ëªšëí°ë§
- ê³µì ìì€ììë§ ë³Žì ë§í¬ë¥Œ íµíŽ êµ¬ì± ìì ì»êž°
- ì ì§ êŽëЬëì§ ìë ëŒìŽëžë¬ëЬ 몚ëí°ë§ (볎ì íšì¹ ìì)
- íšì¹ ê³í ì늜: ì ë°ìŽížë¥Œ ì ìíê² í ì€ížíê³ ë°°í¬
8. A07: ìë³ ë° ìžìŠ ì€íš¶
8.1 ì€ëª ¶
ì¬ì©ì ì ì íìž, ìžìŠ ë° ìžì êŽëЬë ìžìŠ êŽë š 공격ìŒë¡ë¶í° 볎ížíë ë° ì€ìí©ëë€. ìŽê²ì ìŽì ì "ì·šìœí ìžìŠ"ìŽëŒê³ ë¶ë žìµëë€.
ì°žê³ : í¬êŽì ìž ìžìŠ ëŽì©ì ë ìš 05륌 ì°žì¡°íìžì.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A07: ìë³ ë° ìžìŠ ì€íš â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â ìœì : â
â - ë¬Žì°šë³ ëì
ëë í¬ë ëŽì
ì€í°í íì© â
â - ìœíê±°ë ì ìë €ì§ ë¹ë°ë²íž íì© â
â - ìœí í¬ë ëŽì
복구 ì¬ì© ("ë¹ì ì ì ìë묌 ìŽëŠì?") â
â - í묞 ëë ìœíê² íŽìë ë¹ë°ë²íž ì¬ì© â
â - ë€ì€ ìžìŠ ëëœ ëë ë¹íšìšì â
â - URLì ìžì
ID ë
žì¶ â
â - ë¡ê·žìž í ìžì
ID륌 ìííì§ ìì â
â - ë¡ê·žìì ì ìžì
ì ì ì í 묎íšííì§ ìì â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
8.2 ì·šìœ ìœë vs ìì ë ìœë¶
# ì·šìœ: ë¬Žì°šë³ ëì
ë³Žíž ìì
@app.route('/login', methods=['POST'])
def login_vulnerable():
username = request.json['username']
password = request.json['password']
user = db.find_user(username)
if user and check_password(password, user.password_hash):
session['user_id'] = user.id # ìžì
ì¬ìì± ìì!
return jsonify({"status": "success"})
return jsonify({"error": "Invalid credentials"}), 401
# ìì : ìë ì í, ì êž ë° ìžì
êŽëЬ í¬íš
from flask_limiter import Limiter
limiter = Limiter(app, default_limits=["200 per day"])
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute") # ë¡ê·žìž ìë ìë ì í
def login_secure():
username = request.json['username']
password = request.json['password']
# ê³ì ì êž íìž
if is_account_locked(username):
return jsonify({"error": "ê³ì ìŽ ìŒìì ìŒë¡ ì 게ìµëë€"}), 429
user = db.find_user(username)
if user and check_password(password, user.password_hash):
# ì€íš ìë ì¬ì€ì
reset_failed_attempts(username)
# ìžì
ì¬ìì± (ìžì
ê³ ì ë°©ì§)
session.clear()
session['user_id'] = user.id
session['created_at'] = time.time()
session.permanent = True
return jsonify({"status": "success"})
# ì€íš ìë ìŠê°
record_failed_attempt(username)
# ìŒë° ì€ë¥ (ì¬ì©ì ìŽëŠ ì¡Žì¬ ì¬ë¶ ê³µê° ì íš)
return jsonify({"error": "Invalid credentials"}), 401
8.3 방쎶
- ë€ì€ ìžìŠ êµ¬í (TOTP ëë FIDO2)
- ë¡ê·žìž ìëí¬ìžížì ìë ì í ë° ê³ì ì êž ì¬ì©
- ì ì¶ë ë¹ë°ë²íž ë°ìŽí°ë² ìŽì€ì ë¹ë°ë²íž íìž
- ìì í ë¹ë°ë²íž ì ì¥ ì¬ì© (Argon2id, bcrypt)
- ë¡ê·žìž í ìžì ID ì¬ìì±
- ì ì í ìžì íììì ë° ë¬Žíší 구í
9. A08: ìíížìšìŽ ë° ë°ìŽí° ë¬Žê²°ì± ì€íš¶
9.1 ì€ëª ¶
ìŽ ìë¡ìŽ ì¹Ží ê³ ëŠ¬ë 묎결ì±ì ê²ìŠíì§ ìê³ ìíížìšìŽ ì ë°ìŽíž, ì€ìí ë°ìŽí° ë° CI/CD íìŽíëŒìžì ëí ê°ì ì íë ë° ìŽì ì ë§ì¶¥ëë€. ìŽì "ìì íì§ ìì ìì§ë ¬í" 칎í ê³ ëŠ¬ë¥Œ í¬íší©ëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A08: ìíížìšìŽ ë° ë°ìŽí° ë¬Žê²°ì± ì€íš â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â 공격 벡í°: â
â â
â 1. CI/CD íìŽíëŒìž 칚íŽ: â
â 공격ìê° ë¹ë íìŽíëŒìžì ìì íì¬ ì
ìì ìž ìœë 죌ì
â
â ââââââââ ââââââââ ââââââââ ââââââââ â
â â Code âââââ¶âBuild âââââ¶â Test âââââ¶âDeployâ â
â â â â â â â â â â
â ââââââââ ââââ¬ââââ ââââââââ ââââââââ â
â â â
â ⌠공격ìê° ì¬êž°ì ë°±ëìŽ ì£Œì
â
â â
â 2. ê²ìŠ ìë ìë ì
ë°ìŽíž: â
â ì±ìŽ ëì§íž ìëª
ê²ìŠ ììŽ ì
ë°ìŽíž ë€ìŽë¡ë â
â 공격ìê° MITMì ìííì¬ ì
ìì ìž ì
ë°ìŽíž ì ê³µ â
â â
â 3. ìì íì§ ìì ìì§ë ¬í: â
â ì±ìŽ ì 뢰í ì ìë ë°ìŽí°ë¥Œ ìì§ë ¬ííì¬ RCE ë°ì â
â pickle.loads(user_input) â ì격 ìœë ì€í! â
â â
â 4. ì¢
ìì± íŒë: â
â 공격ìê° ê³µê° ë ì§ì€ížëЬì ëŽë¶ íší€ì§ì ê°ì ìŽëŠìŒë¡ â
â ì
ìì ìž íší€ì§ ê²ì â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
9.2 ì·šìœí ìœë: ìì íì§ ìì ìì§ë ¬í¶
import pickle
import yaml
# ì·šìœ: pickleë¡ ì 뢰í ì ìë ë°ìŽí° ìì§ë ¬í
@app.route('/api/import', methods=['POST'])
def import_data_vulnerable():
data = request.get_data()
obj = pickle.loads(data) # RCE! 공격ìê° ìì ìœë ì€í ê°ë¥
return jsonify({"status": "imported"})
# 공격 íìŽë¡ë:
# import pickle, os
# class Exploit:
# def __reduce__(self):
# return (os.system, ('rm -rf /',))
# pickle.dumps(Exploit())
# ì·šìœ: YAML load (ìì Python ê°ì²Ž íì©)
@app.route('/api/config', methods=['POST'])
def load_config_vulnerable():
config = yaml.load(request.data) # ìì íì§ ìì! ìœë ì€í ê°ë¥
return jsonify(config)
# ìì : ìì í ëì ì¬ì©
import json
@app.route('/api/import', methods=['POST'])
def import_data_secure():
# ë°ìŽí° êµíì pickle ëì JSON ì¬ì©
data = request.get_json()
if not validate_schema(data): # 구조 ê²ìŠ
return jsonify({"error": "ì못ë ë°ìŽí° íì"}), 400
return jsonify({"status": "imported"})
@app.route('/api/config', methods=['POST'])
def load_config_secure():
config = yaml.safe_load(request.data) # safe_loadë ìœë ì€í ì°šëš
return jsonify(config)
9.3 방쎶
- ìíížìšìŽ/ë°ìŽí° 묎결ì±ì ê²ìŠíêž° ìíŽ ëì§íž ìëª ëë ì ì¬í ë°©ë² ì¬ì©
- ëŒìŽëžë¬ëЬ ë° ì¢ ìì±ìŽ ì 뢰í ì ìë ì ì¥ì륌 ì¬ì©íëë¡ ë³Žì¥
- ìíížìšìŽ ê³µêžë§ 볎ì ë구 ì¬ì© (SLSA, Sigstore)
- ë¬Žëš ì¡ìžì€ ëë ìì ì ëíŽ CI/CD íìŽíëŒìž ê²í
- ì 뢰í ì ìë íŽëŒìŽìžížì ìëª ëì§ ìê±°ë ìížíëì§ ìì ì§ë ¬íë ë°ìŽí° ì ì¡ ì íš
- ì 뢰í ì ìë ë°ìŽí°ì
pickle,marshalëëyaml.load()ì ë ì¬ì© ì íš
10. A09: 볎ì ë¡ê¹ ë° ëªšëí°ë§ ì€íš¶
10.1 ì€ëª ¶
ì¶©ë¶í ë¡ê¹ ë° ëªšëí°ë§ìŽ ììŒë©Ž 칚íŽë¥Œ ì ë ê°ì§í ì ììµëë€. ëë¶ë¶ì ì±ê³µì ìž ê³µê²©ì ì·šìœì íììŒë¡ ììíë©°, ìŽë¬í íììŽ ê³ìëëë¡ íì©í멎 ì±ê³µì ìž ì ì© ê°ë¥ì±ìŽ ëìì§ ì ììµëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A09: 볎ì ë¡ê¹
ë° ëªšëí°ë§ ì€íš â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â 묞ì : â
â - ë¡ê·žìž ì€íšê° ë¡ê¹
ëì§ ìì â
â - ê²œê³ ë° ì€ë¥ê° ë¡ê·ž ë©ìì§ë¥Œ ìì±íì§ ìê±°ë ë¶ëª
ííš â
â - ë¡ê·žê° ë¡ì»¬ìë§ ì ì¥ (ìë²ê° 칚íŽë멎 ìì€) â
â - ì늌 ìê³ê° ëë íšê³Œì ìž ìì€ì»¬ë ìŽì
ìì â
â - ì¹ší¬ í
ì€íž ë° DAST ì€ìºìŽ ì늌ì ížëŠ¬ê±°íì§ ìì â
â - ì í늬ìŒìŽì
ìŽ ê³µê²©ì íì§, ìì€ì»¬ë ìŽì
ëë ì늌 ë¶ê° â
â - ë¡ê·ž ìžì ì
: 공격ìê° ê°ì§ ë¡ê·ž í목 ìì± â
â â
â ì¹šíŽ íì§ íê· ìê°: 287ìŒ (IBM 2021) â
â <200ìŒ ëŽ íì§ë ì¹šíŽ ë¹ì©: $3.6M â
â >200ìŒ í íì§ë ì¹šíŽ ë¹ì©: $4.9M â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
10.2 볎ì ë¡ê¹ 구í¶
"""
security_logging.py - í¬êŽì ìž ë³Žì ìŽë²€íž ë¡ê¹
"""
import logging
import json
import time
from flask import Flask, request, g
from datetime import datetime, timezone
app = Flask(__name__)
# ==============================================================
# 볎ì ìŽë²€íž ë¡ê±°
# ==============================================================
class SecurityLogger:
"""구조íë 볎ì ìŽë²€íž ë¡ê¹
."""
def __init__(self, app_name: str):
self.logger = logging.getLogger(f"security.{app_name}")
self.logger.setLevel(logging.INFO)
# 구조íë ë¡ê¹
ì ìí JSON í¬ë§€í°
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(message)s'))
self.logger.addHandler(handler)
def _log_event(self, event_type: str, severity: str, **kwargs):
"""구조íë 볎ì ìŽë²€íž ë¡ê·ž."""
event = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event_type": event_type,
"severity": severity,
"ip_address": request.remote_addr if request else None,
"user_agent": request.headers.get('User-Agent') if request else None,
"user_id": getattr(g, 'current_user', {}).get('id'),
**kwargs
}
self.logger.info(json.dumps(event))
def login_success(self, user_id: str):
self._log_event("auth.login.success", "INFO", user_id=user_id)
def login_failure(self, username: str, reason: str):
self._log_event("auth.login.failure", "WARNING",
username=self._sanitize(username),
reason=reason)
def access_denied(self, resource: str, action: str):
self._log_event("authz.denied", "WARNING",
resource=resource, action=action)
def suspicious_activity(self, description: str, **details):
self._log_event("security.suspicious", "HIGH",
description=description, **details)
def data_access(self, resource_type: str, resource_id: str,
action: str):
self._log_event("data.access", "INFO",
resource_type=resource_type,
resource_id=resource_id, action=action)
def _sanitize(self, value: str) -> str:
"""ë¡ê·ž ìžì ì
ì ë°©ì§íêž° ìíŽ ë¡ê·ž ì
ë ¥ ì ì ."""
if not isinstance(value, str):
return str(value)
# ê°í ë° ì ìŽ ë¬žì ì ê±°
return value.replace('\n', '\\n').replace('\r', '\\r')
sec_log = SecurityLogger("myapp")
# ëŒì°ížìì ì¬ì©
@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
user = authenticate(username, password)
if user:
sec_log.login_success(user.id)
return jsonify({"status": "success"})
else:
sec_log.login_failure(username, "invalid_credentials")
return jsonify({"error": "Invalid credentials"}), 401
# ë¬Žì°šë³ ëì
몚ëí°ë§
@app.before_request
def detect_brute_force():
"""ì ì¬ì ìž ë¬Žì°šë³ ëì
ìë íì§ ë° ì늌."""
if request.path == '/login' and request.method == 'POST':
ip = request.remote_addr
recent_failures = get_recent_login_failures(ip, minutes=5)
if recent_failures >= 10:
sec_log.suspicious_activity(
"ë¬Žì°šë³ ëì
공격 ê°ë¥ì±",
ip_address=ip,
failure_count=recent_failures,
timeframe_minutes=5
)
# ì íì : IP ì°šëš, CAPTCHA ì구, SOC ì늌
10.3 ë¡ê·žì êž°ë¡í ëŽì©¶
| ìŽë²€íž | ì¬ê°ë | í¬íší ìžë¶ ì 볎 |
|---|---|---|
| ë¡ê·žìž ì±ê³µ/ì€íš | INFO/WARNING | ì¬ì©ì ìŽëŠ, IP, íìì€í¬í, ì¬ì©ì ììŽì íž |
| ìžê° ì€íš | WARNING | ì¬ì©ì, 늬ìì€, ìëí ìì |
| ì ë ¥ ê²ìŠ ì€íš | WARNING | ìëí¬ìžíž, ì못ë ì ë ¥ ì í |
| êŽëЬì ìì | INFO | êŽëЬì ì¬ì©ì, ìì , ëì |
| ë¹ë°ë²íž ë³ê²œ | INFO | ì¬ì©ì ID (ë¹ë°ë²ížë ì ë ì ëš) |
| ê³ì ì êž | WARNING | ì¬ì©ì ìŽëŠ, ì€íš íì |
| ë°ìŽí° ëŽë³ŽëŽêž°/ë€ìŽë¡ë | INFO | ì¬ì©ì, ë°ìŽí° ì í, 볌륚 |
| API ìë ì í ížëŠ¬ê±° | WARNING | íŽëŒìŽìžíž, ìëí¬ìžíž, ìë |
| ìì€í ì€ë¥ | ERROR | ì€ë¥ ì í, ì€í ì¶ì (íŽëŒìŽìžížìë ì ëš) |
10.4 방쎶
- 몚ë ë¡ê·žìž, ì ê·Œ ì ìŽ ë° ìë² ìž¡ ì ë ¥ ê²ìŠ ì€íš ë¡ê·ž
- í¬ë ì ë¶ìì ìí ì¶©ë¶í 컚í ì€ížê° ë¡ê·žì ìëì§ íìž
- êž°ê³ íì± ê°ë¥ì±ì ìíŽ êµ¬ì¡°íë ë¡ê¹ ì¬ì© (JSON)
- ì€ì ì§ì€ì ë¡ê·ž êŽëЬ 구í (ELK, Splunk, CloudWatch)
- ìì€ì»¬ë ìŽì ìŽ ìë íšê³Œì ìž ëªšëí°ë§ ë° ì늌 ì€ì
- ìžìëíž ëì ê³í ì늜 ë° ì°ìµ
- ë³ì¡°ë¡ë¶í° ë¡ê·ž ë³Žíž (í ë² ì°êž° ì ì¥ì, ë¬Žê²°ì± íìž)
11. A10: ìë² ìž¡ ìì² ìì¡° (SSRF)¶
11.1 ì€ëª ¶
SSRF ê²°íšì ì¹ ì í늬ìŒìŽì ìŽ ì¬ì©ìê° ì ê³µí URLì ê²ìŠíì§ ìê³ ì격 늬ìì€ë¥Œ ê°ì žì¬ ë ë°ìí©ëë€. ìŽë¥Œ íµíŽ ê³µê²©ìë ì í늬ìŒìŽì ìŽ ë°©í벜, VPN ëë ë€ížìí¬ ACLë¡ ë³Žížëë 겜ì°ìë ììì¹ ëª»í ëììŒë¡ ì¡°ìë ìì²ì 볎ëŽëë¡ ê°ì í ì ììµëë€. ìŽê²ì 2021ë ì ìë¡ìŽ ì¹Ží ê³ ëŠ¬ì ëë€.
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â A10: ìë² ìž¡ ìì² ìì¡° (SSRF) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â ì ì: â
â User âââ¶ Server âââ¶ https://api.external.com/data â
â â
â SSRF 공격: â
â User âââ¶ Server âââ¶ http://169.254.169.254/metadata â
â (AWS ìžì€íŽì€ ë©íë°ìŽí°!) â
â â
â User âââ¶ Server âââ¶ http://localhost:6379/ â
â (ëŽë¶ Redis ìë²!) â
â â
â User âââ¶ Server âââ¶ http://10.0.0.5:8080/admin â
â (ëŽë¶ êŽëЬì íšë!) â
â â
â User âââ¶ Server âââ¶ file:///etc/passwd â
â (ë¡ì»¬ íìŒ ìœêž°!) â
â â
â ìí¥: â
â - íŽëŒì°ë ìžì€íŽì€ ë©íë°ìŽí° ì ê·Œ (ì격 ìŠëª
ëì©) â
â - ëŽë¶ ë€ížìí¬ ì€ìº â
â - ëŽë¶ ìë¹ì€ ì ê·Œ (Redis, ë°ìŽí°ë² ìŽì€, êŽëЬì íšë) â
â - ë¡ì»¬ íìŒ ìœêž° â
â - Capital One ì¹šíŽ (2019): SSRF â ë©íë°ìŽí° â S3 ì ê·Œ â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
11.2 ì·šìœí ìœë¶
import requests
from urllib.parse import urlparse
# ì·šìœ: ìì URL ê°ì žì€êž°
@app.route('/api/fetch-url', methods=['POST'])
def fetch_url_vulnerable():
url = request.json['url']
# ê²ìŠ ìì! ì¬ì©ìê° ëŽë¶ ìë¹ì€ì ì ê·Œ ê°ë¥
response = requests.get(url)
return jsonify({"content": response.text})
# 공격 ìì:
# {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
# {"url": "http://localhost:6379/CONFIG SET dir /tmp"}
# {"url": "file:///etc/passwd"}
# ì·šìœ: ê²ìŠ ìë ìŽë¯žì§ íë¡ì
@app.route('/api/proxy-image')
def proxy_image_vulnerable():
image_url = request.args.get('url')
response = requests.get(image_url)
return response.content, 200, {'Content-Type': response.headers.get('Content-Type')}
11.3 ìì ë ìœë¶
import ipaddress
import socket
from urllib.parse import urlparse
import requests
# íì©ë ëë©ìžì íìŽížëЬì€íž
ALLOWED_DOMAINS = {
"api.example.com",
"images.example.com",
"cdn.trusted-partner.com",
}
# ì°šëšë IP ë²ì (ëŽë¶ ë€ížìí¬)
BLOCKED_IP_RANGES = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"), # Link-local (AWS ë©íë°ìŽí°)
ipaddress.ip_network("0.0.0.0/8"),
ipaddress.ip_network("100.64.0.0/10"), # Carrier-grade NAT
ipaddress.ip_network("fd00::/8"), # IPv6 private
ipaddress.ip_network("::1/128"), # IPv6 loopback
]
def is_safe_url(url: str) -> bool:
"""URLìŽ ê°ì žì€êž°ì ìì íì§ ê²ìŠ."""
try:
parsed = urlparse(url)
# HTTP(S) ì€íŽë§ íì©
if parsed.scheme not in ('http', 'https'):
return False
hostname = parsed.hostname
if not hostname:
return False
# ìµì
A: íìŽížëЬì€íž ì ê·Œ (ê°ì¥ ê°ë ¥)
if hostname not in ALLOWED_DOMAINS:
return False
# ìµì
B: ëžë늬ì€íž ì ê·Œ (íìŽížëЬì€ížê° ë¶ê°ë¥í 겜ì°)
# ížì€íž ìŽëŠì IPë¡ íŽìíê³ ì°šëšë ë²ìì ë¹êµ
resolved_ips = socket.getaddrinfo(hostname, None)
for family, _, _, _, sockaddr in resolved_ips:
ip = ipaddress.ip_address(sockaddr[0])
for blocked_range in BLOCKED_IP_RANGES:
if ip in blocked_range:
return False
return True
except (ValueError, socket.gaierror):
return False
# ìì : ê°ì žì€êž° ì URL ê²ìŠ
@app.route('/api/fetch-url', methods=['POST'])
def fetch_url_secure():
url = request.json.get('url', '')
if not is_safe_url(url):
return jsonify({"error": "URLìŽ íì©ëì§ ììµëë€"}), 400
try:
response = requests.get(
url,
timeout=5,
allow_redirects=False, # ëŽë¶ ìë¹ì€ë¡ì 늬ëë ì
ë°©ì§
)
# 늬ëë ì
ìŽ ìë ê²œì° ëìë ê²ìŠ
if response.is_redirect:
redirect_url = response.headers.get('Location', '')
if not is_safe_url(redirect_url):
return jsonify({"error": "ì°šëšë URLë¡ì 늬ëë ì
"}), 400
return jsonify({"content": response.text[:10000]}) # ìëµ í¬êž° ì í
except requests.RequestException as e:
return jsonify({"error": "URLì ê°ì žì€ì§ 못íìµëë€"}), 400
11.4 방쎶
- 몚ë íŽëŒìŽìžíž ì ê³µ ì ë ¥ URLì ì ì íê³ ê²ìŠ
- êžì ì íì© ëª©ë¡ìŒë¡ URL ì€íŽ, í¬íž ë° ëì ê°ì
- ìì ìëµì íŽëŒìŽìžížì 볎ëŽì§ ìêž°
- HTTP 늬ëë ì ë¹íì±í
- ë€ížìí¬ ìì€ ìžê·žëšŒí ìŽì ì¬ì© (ìë²-ëŽë¶ ížëíœì ë°©ì§íë ë°©í벜 ê·ì¹)
- íŽëŒì°ë í겜ì 겜ì°: ìžì€íŽì€ ë©íë°ìŽí°ì ëíŽ IMDSv1 ëì IMDSv2 ì¬ì© (í í° íì)
12. ë°©ìŽ ì²Ží¬ëЬì€íž¶
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â OWASP Top 10 ë°©ìŽ ì²Ží¬ëЬì€íž â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â A01 - ì·šìœí ì ê·Œ ì ìŽ: â
â [ ] Ʞ볞 ê±°ë¶ ì ê·Œ ì ìŽ â
â [ ] 늬ìì€ ìì ê¶ ê²ìŠ â
â [ ] 믌ê°í ìëí¬ìžížì ìë ì í â
â [ ] CORS ì ì í êµ¬ì± â
â â
â A02 - ìíží ì€íš: â
â [ ] 믌ê°ëë³ë¡ ë°ìŽí° ë¶ë¥ â
â [ ] 몚ë ì ì¡ ì€ ë°ìŽí°ì TLS 1.2+ â
â [ ] ì ì¥ ì€ ë°ìŽí°ì AES-256-GCM ëë ChaCha20 â
â [ ] ë¹ë°ë²ížì Argon2id/bcrypt â
â â
â A03 - ìžì ì
: â
â [ ] 몚ë ê³³ìì ë§€ê°ë³ìíë 쿌늬 â
â [ ] ë°ìŽí°ë² ìŽì€ ì ê·Œì ORM ì¬ì© â
â [ ] ì
ë ¥ ê²ìŠ (íìŽížëЬì€íž ë°©ì) â
â [ ] 컚í
ì€ížì ë§ë ì¶ë ¥ ìžìœë© â
â â
â A04 - ìì íì§ ìì ì€ê³: â
â [ ] ìí 몚ëžë§ ìí â
â [ ] ì구ì¬íì ëšì© ì¬ë¡ í¬íš â
â [ ] 믌ê°í íë¡ì°ì ìë ì í, CAPTCHA â
â [ ] 볎ì ì€ê³ 늬뷰 â
â â
â A05 - 볎ì êµ¬ì± ì€ë¥: â
â [ ] ê° í겜ì ëí ê°í 첎í¬ëЬì€íž â
â [ ] íë¡ëì
ìì ëë²ê·ž 몚ë OFF â
â [ ] 볎ì í€ë êµ¬ì± â
â [ ] Ʞ볞 í¬ë ëŽì
ìì â
â â
â A06 - ì·šìœí êµ¬ì± ìì: â
â [ ] CI/CDìì ì¢
ìì± ì€ìº â
â [ ] ì êž°ì ìž ì
ë°ìŽíž ë° íšì¹ â
â [ ] ì 뢰í ì ìë ìì€ì êµ¬ì± ììë§ â
â [ ] SBOM(ìíížìšìŽ ìì¬ ëª
ìžì) ì ì§ â
â â
â A07 - ìžìŠ ì€íš: â
â [ ] MFA íì±í (í¹í êŽëЬì ê³ì ) â
â [ ] ì€íší ìë í ê³ì ì êž â
â [ ] ë¡ê·žìž í ìžì
ì¬ìì± â
â [ ] ì ì¶ë ë¹ë°ë²íž íìž â
â â
â A08 - ë¬Žê²°ì± ì€íš: â
â [ ] ì
ë°ìŽíž/ë°°í¬ì ëí ëì§íž ìëª
â
â [ ] ì ê·Œ ì ìŽë¡ CI/CD íìŽíëŒìž 볎ì â
â [ ] ì 뢰í ì ìë ë°ìŽí°ì ìì§ë ¬í êžì§ â
â [ ] ìžë¶ ì€í¬ëŠœížì ëí SRI(íì 늬ìì€ ë¬Žê²°ì±) â
â â
â A09 - ë¡ê¹
ë° ëªšëí°ë§: â
â [ ] ì¶©ë¶í 컚í
ì€ížë¡ 볎ì ìŽë²€íž ë¡ê¹
â
â [ ] ì€ì ì§ì€ì ë¡ê·ž êŽëЬ â
â [ ] ìì¬ì€ë¬ìŽ íšíŽì ëí ì늌 â
â [ ] ìžìëíž ëì ê³í í
ì€íž â
â â
â A10 - SSRF: â
â [ ] URL ê²ìŠ (íìŽížëЬì€íž ì íž) â
â [ ] ë€ížìí¬ ìžê·žëšŒí
ìŽì
â
â [ ] ë¶íìí URL ì€íŽ ë¹íì±í â
â [ ] íŽëŒì°ë ë©íë°ìŽí°ì IMDSv2 â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
13. ì°ìµ ë¬žì ¶
ì°ìµ 묞ì 1: ì·šìœì ìë³¶
ë€ì Flask ì í늬ìŒìŽì ì ê²í íê³ ê° ì·šìœì ì íŽë¹íë OWASP Top 10 칎í ê³ ëŠ¬ë¥Œ ìë³íìžì.
"""
ì°ìµ 묞ì : ë²ížê° ë§€ê²šì§ ê° ë¬žì ì ëí OWASP Top 10 칎í
ê³ ëŠ¬ë¥Œ ìë³íìžì.
ìŒë¶ ì€ìë ì¬ë¬ 묞ì ê° ìì ì ììµëë€.
"""
from flask import Flask, request, jsonify, send_file
import pickle
import os
import sqlite3
import yaml
import requests
app = Flask(__name__)
app.config['DEBUG'] = True # 묞ì 1: ???
app.config['SECRET_KEY'] = 'development' # 묞ì 2: ???
@app.route('/api/search')
def search():
q = request.args.get('q')
conn = sqlite3.connect('app.db')
cursor = conn.execute(
f"SELECT * FROM products WHERE name LIKE '%{q}%'" # 묞ì 3: ???
)
return jsonify(cursor.fetchall())
@app.route('/api/user/<int:user_id>')
def get_user(user_id): # 묞ì 4: ???
user = db.get_user(user_id)
return jsonify(user)
@app.route('/api/import', methods=['POST'])
def import_data():
data = pickle.loads(request.data) # 묞ì 5: ???
return jsonify({"status": "imported"})
@app.route('/api/fetch', methods=['POST'])
def fetch():
url = request.json['url']
resp = requests.get(url) # 묞ì 6: ???
return resp.text
@app.route('/api/config', methods=['POST'])
def load_config():
config = yaml.load(request.data) # 묞ì 7: ???
return jsonify(config)
@app.route('/login', methods=['POST'])
def login():
username = request.json['username']
password = request.json['password']
user = db.find_user(username)
if user and user.password == password: # 묞ì 8: ???
session['user'] = user.id
return jsonify({"status": "ok"})
return jsonify({"error": f"User {username} not found or wrong password"}), 401 # 묞ì 9: ???
@app.errorhandler(500)
def error(e):
return jsonify({
"error": str(e),
"trace": traceback.format_exc() # 묞ì 10: ???
}), 500
ì°ìµ 묞ì 2: ìì í ì í늬ìŒìŽì ì€ê³¶
íìŒ ê³µì ì í늬ìŒìŽì ì ëí 볎ì ì ìŽë¥Œ ì€ê³íê³ êµ¬ííìžì.
"""
ì°ìµ 묞ì : ê° OWASP Top 10 칎í
ê³ ëŠ¬ì ëí 볎ì ì ìŽë¥Œ 구ííìžì.
ì í늬ìŒìŽì
ì ì¬ì©ìê° íìŒì ì
ë¡ë, ê³µì ë° ë€ìŽë¡ëí ì ììµëë€.
"""
class SecureFileSharing:
def upload_file(self, user_id: str, file_data: bytes,
filename: str) -> dict:
"""
ìì í íìŒ ì
ë¡ë.
ê³ ë € ì¬í: A03 (íìŒ ìŽëŠì íµí ìžì ì
), A04 (íìŒ í¬êž° ì í),
A05 (íìŒ ì í ê²ìŠ), A08 (ë¬Žê²°ì± íìž)
"""
pass
def share_file(self, owner_id: str, file_id: str,
target_user_id: str, permissions: list) -> bool:
"""
ë€ë¥ž ì¬ì©ìì íìŒ ê³µì .
ê³ ë € ì¬í: A01 (ì ê·Œ ì ìŽ), A04 (ê³µì ì í)
"""
pass
def download_file(self, user_id: str, file_id: str) -> bytes:
"""
íìŒ ë€ìŽë¡ë.
ê³ ë € ì¬í: A01 (ì ê·Œ ì ìŽ), A09 (ë¡ê¹
),
A10 (íìŒìŽ ìžë¶ URLì ì°žì¡°íë 겜ì°)
"""
pass
def fetch_external_file(self, url: str) -> bytes:
"""
ìžë¶ URLìì íìŒ ê°ì žì€êž°.
ê³ ë € ì¬í: A10 (SSRF), A06 (URL ëŒìŽëžë¬ëЬ ê²ìŠ)
"""
pass
ì°ìµ 묞ì 3: 볎ì ê°ì¬ ë³Žê³ ì¶
ì뮬ë ìŽì ë 볎ì ê°ì¬ ìí:
ì°ìµ 묞ì : ë€ì곌 ê°ì í¹ì±ì ê°ì§ ì¹ ì í늬ìŒìŽì
ìŽ ì£ŒìŽì¡ì ë:
- Python/Flask ë°±ìë
- PostgreSQL ë°ìŽí°ë² ìŽì€
- JWT ìžìŠ
- íìŒ ì
ë¡ë êž°ë¥
- ì¹í
íµí© (ìžë¶ URL ê°ì žì€êž°)
- 15ê°ì Python ì¢
ìì± ì¬ì© (6ê°ì ëì ê°ì¬ëì§ ìì)
- ë¡ì»¬ íìŒìë§ êž°ë¡ë ë¡ê·ž
- ìë ì í ìì
- AWS EC2ìì ì€í
ê° OWASP Top 10 칎í
ê³ ëŠ¬ì ëíŽ:
1. ìŽ ì í늬ìŒìŽì
ì ëí í¹ì ìí ìë³
2. ìí ë±êž íê° (Critical/High/Medium/Low)
3. 구첎ì ìž íŽê²° ëšê³ ì ì
4. 구í ë
žë ¥ ì¶ì
ë°ê²¬ ì¬íì 구조íë 볎ì ê°ì¬ ë³Žê³ ìë¡ ìì±íìžì.
ì°ìµ 묞ì 4: ì·šìœí ì í늬ìŒìŽì ìì ¶
ì°ìµ 묞ì 1ì ìœë륌 ê°ì žìì 몚ë ì·šìœì ì ìì íì¬ ë€ì ìì±íìžì. ìì ë ë²ì ì 몚ë OWASP Top 10 칎í ê³ ëŠ¬ë¥Œ ë€ë£šìŽìŒ í©ëë€.
ì°ìµ 묞ì 5: OWASP Top 10 ë§€í¶
ë€ì ì€ì ì¹šíŽ ì¬ë¡ë¥Œ OWASP Top 10 칎í ê³ ëŠ¬ì ë§€ííìžì.
1. Equifax (2017) - íšì¹ëì§ ìì Apache Struts ì·šìœì
â A0?: ___
2. Capital One (2019) - AWS ë©íë°ìŽí° ì ê·Œì ìí SSRF
â A0?: ___
3. SolarWinds (2020) - 칚íŽë ë¹ë íìŽíëŒìž
â A0?: ___
4. Facebook (2019) - 볎ížëì§ ìì S3ì 5ìµ 4ì²ë§ ì¬ì©ì ë ìœë
â A0?: ___
5. Uber (2016) - GitHub ì ì¥ìì íëìœë©ë AWS ì격 ìŠëª
â A0?: ___
6. British Airways (2018) - ê²°ì íìŽì§ì Magecart XSS
â A0?: ___
7. Marriott (2018) - 4ë
ëì ê°ì§ëì§ ìì 칚íŽ
â A0?: ___
8. Yahoo (2013-2014) - ì¬ì©ì ë°ìŽí°ì ìœí/ìíží ìì
â A0?: ___
14. ì윶
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â OWASP Top 10 (2021) ììœ â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â â
â A01: ì·šìœí ì ê·Œ ì ìŽ - 1ì ìí. Ʞ볞 ê±°ë¶. â
â A02: ìíží ì€íš - 믌ê°í 몚ë ê²ì ìíží. â
â A03: ìžì ì
- 몚ë 쿌늬륌 ë§€ê°ë³ìí. â
â A04: ìì íì§ ìì ì€ê³ - 볎ìì ìœëê° ìë ì€ê³ìì ìì. â
â A05: 볎ì êµ¬ì± ì€ë¥ - 몚ë ê²ì ê°í. â
â A06: ì·šìœí êµ¬ì± ìì - ì¢
ìì±ì ìê³ ì
ë°ìŽíž. â
â A07: ìžìŠ ì€íš - MFA, ìë ì í, ê°ë ¥í ë¹ë°ë²íž. â
â A08: ë¬Žê²°ì± ì€íš - 몚ë ê²ì ìëª
, pickle ì¬ì© êžì§. â
â A09: ë¡ê¹
ì€íš - 볎ì ìŽë²€íž ë¡ê¹
, ì늌, ëì. â
â A10: SSRF - 몚ë URL ê²ìŠ, ë€ížìí¬ ìžê·žëšŒíž. â
â â
â OWASP Top 10ì ììì ìŽì§ ìì í 목ë¡ìŽ ìëëë€. â
â ì í늬ìŒìŽì
볎ìì ìí ìµì êž°ì€ì ìŒë¡ ì¬ì©íìžì. â
â â
â 늬ìì€: â
â - https://owasp.org/Top10/ â
â - OWASP Application Security Verification Standard (ASVS) â
â - OWASP Testing Guide â
â - OWASP Cheat Sheet Series â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ìŽì : 06. ìžê°ì ì ê·Œ ì ìŽ | ë€ì: 08. ìžì ì 공격곌 ë°©ìŽ