가장 중요한 웹 애플리케이션 보안 위험 10가지와 이를 완화하는 방법을 알아보세요.
OWASP Top 10은 웹 애플리케이션 보안에 대한 가장 널리 알려진 인식 문서입니다. 2025년판은 최신 위협 환경을 반영하여 소프트웨어 공급망 장애 및 예외 조건의 잘못된 처리와 같은 새로운 카테고리를 도입했습니다.
액세스 제어는 사용자가 의도된 권한을 벗어난 행동을 할 수 없도록 정책을 시행합니다. 실패하면 일반적으로 데이터의 무단 정보 공개, 수정 또는 파기 또는 사용자의 한계를 벗어난 비즈니스 기능 수행으로 이어집니다.
공격자는 액세스 제어 결함을 악용하여 다른 사용자의 계정에 액세스하거나, 민감한 파일을 보거나, 다른 사용자의 데이터를 수정하거나, 액세스 권한을 변경할 수 있습니다.
# User ID taken directly from request without authorization check @app.route('/api/user/<user_id>/profile') def get_profile(user_id): user = db.get_user(user_id) # No ownership check! return jsonify(user.to_dict())
@app.route('/api/user/<user_id>/profile') @login_required def get_profile(user_id): # Verify the requesting user owns this resource if current_user.id != user_id and not current_user.is_admin: abort(403) user = db.get_user(user_id) if not user: abort(404) return jsonify(user.to_dict())
보안 설정이 잘못 정의, 구현 또는 유지 관리되면 보안 설정이 잘못 구성되는 경우가 발생합니다. 여기에는 보안 강화 누락, 불필요한 기능 활성화, 변경되지 않은 비밀번호가 있는 기본 계정, 지나치게 장황한 오류 메시지, 잘못 구성된 HTTP 보안 헤더 등이 포함됩니다.
잘못 구성된 서버, 프레임워크 또는 클라우드 서비스는 민감한 데이터를 노출하거나 무단 액세스를 가능하게 하거나 공격자에게 추가 공격을 계획할 수 있는 정보를 제공할 수 있습니다.
# Debug mode enabled in production, verbose errors exposed app = Flask(__name__) app.config['DEBUG'] = True # Exposes stack traces! app.config['SECRET_KEY'] = 'default-secret' # Default key!
import os app = Flask(__name__) app.config['DEBUG'] = False app.config['SECRET_KEY'] = os.environ['SECRET_KEY'] @app.after_request def set_security_headers(response): response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = "default-src 'self'" return response
2025년에 새로 추가되었습니다. 타사 구성 요소, 종속성 및 CI/CD 파이프라인과 관련된 위험에 중점을 둡니다. 공격자는 패키지를 손상시키거나 오픈 소스 라이브러리에 악성 코드를 삽입하거나 빌드 파이프라인 취약점을 악용하여 소프트웨어 공급망을 표적으로 삼습니다.
손상된 하나의 종속성이 수천 개의 애플리케이션에 영향을 미칠 수 있습니다. 공급망 공격은 데이터 도난, 백도어 설치 또는 최소한의 탐지로도 완전한 시스템 손상으로 이어질 수 있습니다.
// package.json with unpinned dependencies { "dependencies": { "express": "*", // Any version! "lodash": "^4.0.0", // Wide range "unknown-pkg": "^1.0.0" // Unvetted package } }
// package.json with pinned versions + lockfile + audit { "dependencies": { "express": "4.21.2", "lodash": "4.17.21" }, "scripts": { "preinstall": "npx npm-audit-resolver", "integrity-check": "npm audit signatures" } } // Also: use package-lock.json, enable Dependabot/Renovate, // generate SBOM, verify package provenance
암호화와 관련된 실패로 인해 민감한 데이터가 노출되는 경우가 많습니다. 여기에는 더 이상 사용되지 않는 알고리즘 사용, 취약한 키 생성, 전송 중이거나 미사용 중인 데이터에 대한 암호화 누락, 부적절한 인증서 유효성 검사 등이 포함됩니다.
암호화가 약하거나 누락되면 비밀번호, 신용카드 번호, 건강 기록, 개인 정보, 비즈니스 기밀이 노출되어 규제 위반(GDPR, PCI DSS)으로 이어질 수 있습니다.
import hashlib # Storing passwords with weak hashing def store_password(password: str) -> str: return hashlib.md5(password.encode()).hexdigest() # MD5 is broken!
import bcrypt def store_password(password: str) -> bytes: # Use bcrypt with automatic salting salt = bcrypt.gensalt(rounds=12) return bcrypt.hashpw(password.encode(), salt) def verify_password(password: str, hashed: bytes) -> bool: return bcrypt.checkpw(password.encode(), hashed)
인젝션 결함은 신뢰할 수 없는 데이터가 명령이나 쿼리의 일부로 인터프리터에 전송될 때 발생합니다. SQL 인젝션, NoSQL 인젝션, OS 명령 인젝션, 크로스 사이트 스크립팅(XSS)이 가장 일반적인 형태입니다. 적대적인 데이터는 인터프리터를 속여 의도하지 않은 명령을 실행하도록 할 수 있습니다.
인젝션은 데이터 손실, 손상, 무단 액세스, 완전한 호스트 장악 또는 서비스 거부를 초래할 수 있습니다. SQL 인젝션은 여전히 가장 위험하고 널리 퍼진 공격 기법 중 하나입니다.
# SQL injection via string concatenation def get_user(username: str): query = f"SELECT * FROM users WHERE name = '{username}'" return db.execute(query) # username = "' OR '1'='1"
# Parameterized queries prevent SQL injection def get_user(username: str): query = "SELECT * FROM users WHERE name = %s" return db.execute(query, (username,)) # For XSS prevention, escape output from markupsafe import escape def render_comment(comment: str) -> str: return f"<p>{escape(comment)}</p>"
안전하지 않은 설계는 설계 및 아키텍처 결함과 관련된 위험을 의미합니다. 따라서 위협 모델링, 보안 설계 패턴, 참조 아키텍처를 사용해야 합니다. 안전하지 않은 설계는 완벽한 구현으로 해결할 수 없으며, 특정 공격을 방어하기 위해 필요한 보안 제어가 만들어지지 않았기 때문입니다.
설계 결함은 자동화된 도구로는 탐지하기 어려운 비즈니스 로직 취약점으로 이어질 수 있습니다. 민감한 작업에 대한 속도 제한이 없거나 중요한 작업에 대한 다단계 인증이 없거나 사기 탐지가 불충분한 경우 모두 설계 수준의 결함입니다.
# Password reset with no rate limit or verification @app.route('/reset-password', methods=['POST']) def reset_password(): email = request.form['email'] new_pass = request.form['new_password'] user = db.find_by_email(email) user.password = hash_password(new_pass) # No token verification! db.save(user)
from datetime import datetime, timedelta @app.route('/reset-password', methods=['POST']) @rate_limit("3/hour") def reset_password(): token = request.form['token'] new_pass = request.form['new_password'] # Verify time-limited, single-use token reset_req = db.find_reset_token(token) if not reset_req or reset_req.used or reset_req.expires < datetime.utcnow(): abort(400, "Invalid or expired token") # Enforce password complexity if not meets_password_policy(new_pass): abort(400, "Password does not meet requirements") reset_req.user.password = hash_password(new_pass) reset_req.used = True db.save_all([reset_req.user, reset_req])
사용자의 신원 확인, 인증 및 세션 관리는 매우 중요합니다. 애플리케이션에서 자격 증명 스터핑, 무차별 대입, 취약한 비밀번호를 허용하거나 로그인 후 세션 ID가 회전하지 않는 등 세션 관리에 결함이 있는 경우 인증 실패가 발생합니다.
공격자는 자동화된 크리덴셜 스터핑, 무차별 대입 또는 세션 하이재킹을 통해 사용자 계정에 액세스할 수 있습니다. 손상된 계정은 신원 도용, 사기, 데이터 유출로 이어질 수 있습니다.
# No brute force protection, weak session handling @app.route('/login', methods=['POST']) def login(): user = db.find_by_email(request.form['email']) if user and user.password == request.form['password']: # Plain comparison! session['user'] = user.id # Session ID not rotated return redirect('/dashboard')
from flask_limiter import Limiter limiter = Limiter(app, default_limits=["100/hour"]) @app.route('/login', methods=['POST']) @limiter.limit("5/minute") def login(): user = db.find_by_email(request.form['email']) if not user or not bcrypt.checkpw( request.form['password'].encode(), user.password_hash ): # Generic error to prevent user enumeration return "Invalid credentials", 401 # Rotate session ID after authentication session.regenerate() session['user'] = user.id # Check for MFA requirement if user.mfa_enabled: return redirect('/mfa-verify') return redirect('/dashboard')
소프트웨어 및 데이터 무결성 장애는 무결성 위반으로부터 보호하지 못하는 코드 및 인프라와 관련이 있습니다. 여기에는 안전하지 않은 역직렬화, 무결성 검증 없이 신뢰할 수 없는 CDN 또는 플러그인 사용, 서명된 업데이트가 없는 자동 업데이트 메커니즘이 포함됩니다.
안전하지 않은 역직렬화는 원격 코드 실행으로 이어질 수 있습니다. SRI(하위 리소스 무결성)가 없는 신뢰할 수 없는 CDN에서 스크립트를 로드하면 공격자가 애플리케이션에 악성 코드를 삽입할 수 있습니다.
<!-- Loading external scripts without integrity checks --> <script src="https://cdn.example.com/lib.js"></script> <!-- Insecure deserialization in Python --> import pickle data = pickle.loads(user_input) # Arbitrary code execution!
<!-- Use Subresource Integrity (SRI) for external scripts --> <script src="https://cdn.example.com/lib.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K..." crossorigin="anonymous"></script> # Use safe deserialization in Python import json data = json.loads(user_input) # Safe: only parses JSON data
충분한 로깅, 모니터링, 알림이 없으면 침해를 적시에 탐지할 수 없습니다. 불충분한 로깅, 사고 대응 시스템과의 비효율적인 통합, 실시간 알림의 부재는 공격자가 시스템을 추가로 공격하고 지속성을 유지하며 데이터를 변조하거나 추출할 수 있도록 합니다.
적절한 로깅이 없으면 공격자는 오랜 기간 동안 탐지되지 않고 활동할 수 있습니다. 대부분의 침해 연구에 따르면 침해를 탐지하는 데 걸리는 평균 시간은 200일을 초과하며, 내부 모니터링이 아닌 외부 당사자에 의해 발견되는 경우가 많습니다.
# No logging of security-relevant events @app.route('/login', methods=['POST']) def login(): user = authenticate(request.form) if not user: return "Login failed", 401 # No record of failure return redirect('/dashboard')
import logging from datetime import datetime security_log = logging.getLogger('security') @app.route('/login', methods=['POST']) def login(): user = authenticate(request.form) if not user: security_log.warning( "LOGIN_FAILED | ip=%s | email=%s | time=%s", request.remote_addr, request.form.get('email', 'unknown'), datetime.utcnow().isoformat(), ) check_brute_force(request.remote_addr) return "Invalid credentials", 401 security_log.info( "LOGIN_SUCCESS | user_id=%s | ip=%s", user.id, request.remote_addr, ) return redirect('/dashboard')
2025년 신규 기능. 오류, 예외 및 엣지 케이스를 부적절하게 처리하는 애플리케이션은 민감한 정보를 노출하거나 일관되지 않은 상태로 들어가거나 악용 가능한 조건을 만들 수 있습니다. 여기에는 프로덕션 환경의 장황한 오류 메시지, 보안 제어를 우회하는 잡히지 않은 예외, 경쟁 조건 등이 포함됩니다.
부적절한 오류 처리로 인해 스택 추적, 데이터베이스 세부 정보 또는 내부 경로가 노출될 수 있습니다. 보안 검사의 경합 조건은 인증 또는 결제 유효성 검사를 우회하는 TOCTOU(Time-of-Check-to-Time-of-Use) 공격을 허용할 수 있습니다.
# Exposing internal details in error messages @app.route('/api/data') def get_data(): try: result = db.query(request.args['q']) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 # Leaks DB details!
import uuid, logging logger = logging.getLogger(__name__) @app.errorhandler(Exception) def handle_exception(e): error_id = str(uuid.uuid4()) logger.error("Unhandled exception [%s]: %s", error_id, e, exc_info=True) # Return generic error with reference ID for support return jsonify({ "error": "An internal error occurred", "reference": error_id, }), 500 @app.route('/api/data') def get_data(): q = request.args.get('q') if not q or not is_valid_query(q): return jsonify({"error": "Invalid query parameter"}), 400 result = db.query(q) return jsonify(result)
| ID | 취약성 | 심각도 | 주요 완화 |
|---|---|---|---|
| A01 | 고장난 액세스 제어 | Critical | 서버 측 액세스 확인, 기본 거부, 소유권 기록 |
| A02 | 보안 구성 오류 | High | 강화 프로세스, 보안 헤더, 기본값 비활성화 |
| A03 | 소프트웨어 공급망 장애 | High | 고정된 종속성, SBOM, 종속성 검색 |
| A04 | 암호화 실패 | High | 강력한 알고리즘, TLS 1.2+, 키 관리 |
| A05 | 주입 | Critical | 매개변수화된 쿼리, 출력 인코딩, 입력 유효성 검사 |
| A06 | 안전하지 않은 디자인 | High | 위협 모델링, 보안 패턴, 악용 사례 테스트 |
| A07 | 인증 실패 | High | MFA, 무차별 대입 보호, 세션 로테이션 |
| A08 | 소프트웨어 또는 데이터 무결성 장애 | High | SRI, 서명된 업데이트, 안전한 역직렬화 |
| A09 | 보안 로깅 및 실패 알림 | Medium | 중앙 집중식 로깅, 실시간 알림, 사고 대응 |
| A10 | 예외 조건의 잘못된 처리 | Medium | 글로벌 오류 처리기, 일반 메시지, TOCTOU 방지 |