어떤 수업을 찾고 있나요?

예: PowerShell, 네트워크, XSS, Step 138

전체 커리큘럼 둘러보기 →

개발 도구 · 프로젝트

Step 94. Flask 맛보기 — 웹 서버를 직접 만든다

Step 94

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간

전제: Step 73의 HTTP 지식, Step 41~46의 파이썬 기초를 마쳤다.

  • 준비물: 파이썬과 pip install flask. 이 챕터의 실측은 Flask 3.1.3, 127.0.0.1(localhost)에서 수행했습니다.
  • 주의: 오늘부터 여러분은 요청을 보내는 쪽(클라이언트)이 아니라 받는 쪽(서버) 을 직접 만듭니다. 모든 실습은 내 컴퓨터 안의 개발용 서버로 진행하고 외부에 공개하지 않습니다. 후반에 일부러 취약한 페이지를 만드는 실험이 있습니다 — 내 서버, 내 브라우저 안에서만입니다.

Step 73에서 우리는 HTTP 요청을 보내는 법을 배웠습니다. 그런데 한 가지가 빠져 있었습니다 — 요청을 받은 쪽에서는 무슨 일이 일어나는가? 오늘 그 블랙박스를 엽니다. 도구는 Flask(플라스크) — 파이썬 몇 줄로 웹 서버를 띄울 수 있는, 가장 작은 웹 프레임워크입니다. 취약점은 추상 개념이 아니라 누군가가 짠 코드 속의 구멍입니다. 그래서 취약한 코드는 내가 직접 짜 봅니다 — 오늘의 마지막 장면에서 여러분은 처음으로 XSS(크로스 사이트 스크립팅)를 손끝으로 체감하게 됩니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • Flask로 라우트 2개 이상의 서버를 띄울 수 있다
  • 쿼리 문자열(GET)과 폼 데이터(POST)를 각각 받아 처리할 수 있다
  • 딕셔너리를 반환해 JSON API를 만들 수 있다
  • 브라우저 Network 탭과 서버 로그를 대조해 요청을 추적할 수 있다
  • XSS가 발생하는 코드와 그 원리(코드·데이터 혼합), 그리고 이스케이프 방어를 설명할 수 있다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3 + Flask 3.x, localhost(127.0.0.1) 개발 서버
오늘의 코드 @app.route(라우팅), request.args(GET 파라미터), request.form(POST 폼), 딕셔너리 반환(JSON), markupsafe.escape(이스케이프)
필요한 개념 라우팅, GET/POST, 쿼리 문자열, 상태 코드(200/404/405), XSS와 이스케이프
오늘의 산출물 미니 방명록 웹 앱 + XSS 성공/차단 대조 캡처

2-1. 프레임워크와 라우팅 — URL을 함수에 연결하기

웹 서버가 하는 일의 본질은 단순합니다. “이 주소로 이 요청이 오면, 이 함수를 실행하고, 그 결과를 돌려줘라.” 주소(URL)와 함수를 연결하는 것을 라우팅(routing)이라고 합니다. Flask에서는 데코레이터(@) 한 줄이 라우팅입니다.

@app.route("/hello")
def hello():
    return "안녕!"

“/hello로 GET 요청이 오면 hello()를 실행해서, 그 반환값을 응답 본문으로 보내라.” 이 두 줄이 웹 서버의 전부이자 시작입니다.

2-2. GET과 POST — 서버 입장에서 다시 보기

Step 73에서 배운 두 메서드를, 이번에는 받는 쪽 시점으로 정리합니다.

  • request.args는 POST를 포함해 어떤 메서드의 URL 쿼리도 읽습니다.
  • request.form은 지원하는 폼 인코딩 본문을 읽습니다. POST는 쿼리와 폼 본문을 함께 가질 수 있습니다.
  • 실제 비밀값을 URL에 넣지 말고 HTTPS 전송 보호와 끝점 로그 제어를 사용하세요.

2-3. 서버 사이드의 시점 — “입력은 전부 의심스럽다”

서버 코드를 짜기 시작하면 세계관이 바뀝니다. 지금까지의 여러분은 브라우저의 사용자였지만, 오늘부터는 “저 요청을 보낸 것이 정말 착한 사용자일까?” 를 묻는 쪽입니다.

서버가 받는 모든 입력 — URL의 파라미터, 폼의 글상자, 쿠키 — 은 사용자가 마음대로 조작할 수 있습니다. 개발자 도구를 열면 누구든 값을 바꿀 수 있으니까요. “서버에 도착하는 입력은 전부 오염되었을 수 있다” — 이 한 문장이 웹 보안의 제1원리이고, 오늘의 XSS 실험이 그 증명입니다.


3. 따라 하기

3-1. 설치와 최소 서버

입력

pip install flask
mkdir flask-lab && cd flask-lab

app.py:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello, Web!"

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000)

출력 (2026-09-09 실측, python app.py):

 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

개발 서버는 루프백 5000 포트에서 대기합니다. 이 실습은 디버그 모드를 끄고 수정 후 수동 재시작합니다. Flask 대화형 디버거는 Python 코드를 실행할 수 있으며 PIN은 운영 보안 경계가 아닙니다. 개발 서버를 운영 배포로 사용하지 마세요.

왜: 다섯 줄로 서버가 돌았습니다. 프레임워크가 번거로운 부분(소켓, HTTP 파싱)을 전부 대신해 주므로, 우리는 “무엇을 보여 줄까”만 생각하면 됩니다.

3-2. 요청의 여행 — 브라우저와 콘솔을 동시에 보기

입력: 브라우저에서 개발자 도구(F12) → Network 탭을 열고, http://127.0.0.1:5000을 새로고침합니다.

출력 (2026-09-09 실측, 서버 콘솔):

127.0.0.1 - - [09/Sep/2026 14:29:13] "GET / HTTP/1.1" 200 -

읽는 법: 같은 사건의 양쪽 얼굴입니다. 브라우저 Network 탭에는 요청 한 줄(상태 200), 서버 콘솔에는 접속 로그 한 줄 — 누가(127.0.0.1), 언제, 무엇을(GET /), 결과가 어땠는지(200)가 찍힙니다.

왜: “요청 하나 = 로그 한 줄”이라는 대응 관계를 몸에 새기는 것이, 나중에 서버 로그로 공격을 추적하는 감각의 밑바탕입니다.

3-3. URL 파라미터 받기 — 쿼리 문자열

입력: 다음 import와 경로를 app.py의 마지막 if __name__ == “__main__” 블록보다 앞에 넣으세요. 이후 경로를 추가해도 app.run 블록은 끝에 둡니다. 저장할 때마다 로컬 서버를 멈추고 다시 실행하세요.

from flask import request
from markupsafe import escape

@app.route("/hello")
def hello():
    name = request.args.get("name", "guest")
    return f"Hello, {escape(name)}!"

출력 (2026-09-09 실측): http://127.0.0.1:5000/hello?name=hacker → 안녕하세요, hacker님!. 파라미터 없이 /hello만 열면 → 안녕하세요, 손님님!.

읽는 법: ?name=hacker의 값을 서버가 꺼내 응답에 실었습니다. request.args.get("name", "손님")의 두 번째 인자는 파라미터가 없을 때의 기본값입니다.

왜: 서버가 사용자 입력을 받아 처리하는 첫 경험입니다. 그리고 눈치채셨겠지만 — 저 name은 사용자가 마음대로 바꿀 수 있습니다. 이 사실이 3-7에서 폭발합니다.

3-4. 폼과 POST — 로그인 흉내

입력: 로그인 페이지와 처리 라우트를 추가합니다.

# Add to the app.py above. Fictional inputs only; this is not authentication.
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        uid = request.form.get("uid", "")
        pw = request.form.get("pw", "")
        return f"Practice ID {escape(uid)} (dummy password length: {len(pw)})"
    return """<form method="post">
      <input name="uid" placeholder="fictional ID">
      <input name="pw" type="password" placeholder="dummy only">
      <button>Submit practice form</button></form>"""

출력 (2026-09-09 실측): /login을 열면(GET) 폼이 보이고, 아이디 tester, 비밀번호 8글자를 넣어 전송하면(POST) 이렇게 돌아옵니다.

아이디 tester 로 로그인 시도 (비밀번호 길이: 8)

읽는 법: 같은 /login 주소인데 메서드에 따라 다른 동작 — GET이면 폼을 보여 주고, POST면 처리합니다. <form method="post">라고 적혀 있어야 데이터가 본문(POST)으로 갑니다. 비밀번호를 화면에 다시 보여 주지 않고 길이만 보여 준 것에 주목하세요 — 받은 비밀을 되돌려 보여 주는 것부터가 나쁜 습관입니다.

이 경로는 가짜 폼 데이터를 돌려줄 뿐 비밀번호를 검증하거나 세션을 만들지 않습니다. 실제 인증에는 별도의 검토된 설계가 필요합니다. 여기서는 폼 표시 GET과 제출 POST를 비교하세요.

3-5. 예측해 보기 — 서버 콘솔에 뭐가 찍힐까

예측: /login 폼을 열고(첫 방문) 전송 버튼까지 누르면 서버 콘솔에는 어떤 로그가 찍힐까요?

  • (a) "GET /login ..." 한 줄
  • (b) "POST /login ..." 한 줄
  • (c) GET과 POST가 각각 한 줄씩

직접 확인 — 출력 (2026-09-09 실측):

127.0.0.1 - - [09/Sep/2026 14:29:13] "GET /login HTTP/1.1" 200 -
127.0.0.1 - - [09/Sep/2026 14:29:13] "POST /login HTTP/1.1" 200 -

정답은 (c) 입니다. “폼을 보는 것”과 “폼을 보내는 것”은 두 번의 별개 요청입니다.

로그는 근거이지만 불완전하거나 오해를 주거나 변조될 수 있습니다. 모든 줄을 신뢰하거나 모든 요청이 기록됐다고 가정하지 말고 브라우저 요청·시간·알고 있는 로컬 활동과 대조하세요.

3-6. JSON 응답 — 화면이 아니라 데이터를 돌려주는 서버

이 API 예제는 흔한 구조화 표현 중 하나인 JSON을 반환합니다. API는 다른 형식도 사용할 수 있습니다. 아래 값은 고정된 시연 데이터이며 열린 포트의 실측값이 아닙니다.

입력

@app.route("/api/status")
def api_status():
    return {"service": "netwatch-lab", "open_ports": [8000], "status": "ok"}

출력 (2026-09-09 실측, http://127.0.0.1:5000/api/status 접속):

{"open_ports":[8000],"service":"netwatch-lab","status":"ok"}

읽는 법: 파이썬 딕셔너리를 return하면 Flask가 자동으로 JSON으로 번역해 줍니다. 브라우저에는 글자덩어리로 보이지만, 이 응답은 다른 프로그램이 기계적으로 읽기 좋은 형태입니다.

Step 95는 명령줄 프로그램으로 구조화된 관찰 데이터를 만듭니다. 이 Flask API를 포함하지 않습니다. 이후 웹 화면을 만들려면 별도 접근 제어와 검토가 필요합니다.

3-7. 취약 맛보기 — XSS의 탄생

이제 일부러 나쁜 코드를 씁니다. 입력을 HTML에 그대로 싣는 라우트입니다.

입력

@app.route("/echo")
def echo():
    name = request.args.get("name", "")
    return f"<h1>입력한 값: {name}</h1>"   # 입력을 HTML에 그대로 삽입 — 취약!

먼저 정상 입력 — /echo?name=길동은 굵은 제목으로 입력한 값: 길동이 보입니다. 이제 공격 입력입니다. 주소창에:

http://127.0.0.1:5000/echo?name=<script>alert('XSS')</script>

출력 (2026-09-09 실측, 서버 응답 본문):

<h1>입력한 값: <script>alert('XSS')</script></h1>

서버는 입력을 가공 없이 그대로 실어 돌려줬고, 이 응답을 받은 브라우저는 <script>를 “내용”이 아니라 “명령”으로 해석해 경고 상자(alert)를 실행합니다. 입력이 데이터가 아니라 스크립트로 실행된 것입니다.

읽는 법: 이것이 XSS(크로스 사이트 스크립팅) — Step 93의 SQL Injection과 정확히 같은 뿌리(코드와 데이터의 혼합)입니다. SQL에서 따옴표가 문법이 됐듯, 여기서는 <script>가 HTML 문법이 됐습니다.

왜: 이 alert 하나가 해킹 대회의 웹 문제 수백 개의 목표물입니다. “입력이 실행된다”는 감각을 내 서버에서 체감했으니, 이 취약점을 정면으로 다룰 준비가 된 것입니다.

⚠️ 보안 연결: 이 실험은 내 컴퓨터의 내 서버에서만 이루어졌습니다. 모든 공격 실습은 내 랩·합법 플랫폼에서만. 실서비스 무단 공격은 범죄입니다. 남의 사이트의 검색창에 이 입력을 넣는 것은 실험이 아니라 공격 시도입니다.

3-8. 방어 맛보기 — 이스케이프 한 줄

취약한 코드를 봤으니 방어도 봅니다. 입력을 안전하게 바꿔 주는 도구가 있습니다.

입력

from markupsafe import escape

@app.route("/echo_safe")
def echo_safe():
    name = request.args.get("name", "")
    return f"<h1>입력한 값: {escape(name)}</h1>"

출력 (2026-09-09 실측, 같은 공격 입력):

<h1>입력한 값: &lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;</h1>

이 HTML 텍스트 맥락에서 escape는 마크업 문자를 인코딩해 예제 스크립트를 문자로 표시합니다. JavaScript·CSS·URL·이후 디코딩 맥락의 범용 방어는 아닙니다. 실제 출력 위치에 맞는 인코딩·검증을 선택하세요.

왜: 공격을 아는 것과 방어를 아는 것은 한 세트입니다. 오늘 여러분은 XSS의 발생과 차단을 30줄 안에서 둘 다 봤습니다 — 이 대조가 기억에 남는 이유입니다.

버전 참고 (2026-09-09 실측, Flask 3.1.3): 예전 자료에 나오는 from flask import escape는 이제 오류가 납니다 — ImportError: cannot import name 'escape' from 'flask'. Flask 2.3부터 제거됐으니, Flask가 내부에서 쓰는 안전 처리 도구인 markupsafe에서 직접 가져오는 위의 형태가 정답입니다.


4. 미션과 연습문제

미션 — 미니 방명록 웹 앱 완성

  1. / — 지금까지 남겨진 메시지 목록을 보여 주는 페이지를 만듭니다.
  2. /write — 이름과 한 줄 메시지를 받는 폼(GET 표시 + POST 처리)을 만듭니다.
  3. POST된 메시지를 파이썬 리스트에 저장해 /에 나열합니다. 단, 출력에는 반드시 escape를 적용합니다.
  4. 일부러 취약한 버전(/echo)을 남겨 두고, XSS 성공 캡처와 escape 적용 후 실패 캡처를 나란히 확보합니다.
  5. 서버 콘솔의 요청 로그와 브라우저 Network 탭을 대조한 화면을 캡처합니다.
  6. 위키에 Flask첫서버.md를 작성합니다 — 라우팅 개념 / GET·POST 차이 / XSS가 생기는 이유 3줄.

연습문제

문제 1. @app.route("/hello") 데코레이터 한 줄이 하는 일을 “주소와 함수의 연결”이라는 말로 설명해 보세요.

연습 2. request.args와 request.form을 구분하세요. 비밀값을 URL에 넣으면 안 되는 이유와 POST 본문에도 HTTPS·신중한 로그 처리가 필요한 이유를 설명하세요.

문제 3. 라우트에 methods=["GET", "POST"]를 빠뜨린 채 폼을 전송하면 어떤 상태 코드가 돌아오나요? 왜 그런지 설명해 보세요.

문제 4. escape가 XSS를 막는 원리를 “코드와 데이터의 혼합”이라는 관점으로 설명해 보세요.


해설과 완료 기준 · 펼치기/접기

5. 모범 답안과 완료 기준

미션 모범 답안

방명록의 뼈대:

"""Local HTML-output exercise; no authentication, CSRF protection, or durability."""
from collections import deque
from threading import Lock
from flask import Flask, request
from markupsafe import escape

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 8192
guestbook = deque(maxlen=100)
lock = Lock()


@app.route("/")
def home():
    with lock:
        entries = list(guestbook)
    items = "".join(
        f"<li><b>{escape(name)}</b>: {escape(msg)}</li>"
        for name, msg in entries)
    return f"<h1>Guestbook</h1><ul>{items}</ul><a href='/write'>Write</a>"


@app.route("/write", methods=["GET", "POST"])
def write():
    if request.method == "POST":
        name = request.form.get("name", "anonymous").strip() or "anonymous"
        msg = request.form.get("msg", "").strip()
        if not msg or len(name) > 64 or len(msg) > 500:
            return "Use a name up to 64 characters and a 1-500 character message", 400
        with lock:
            guestbook.append((name, msg))
        return "Saved. <a href='/'>Back to list</a>"
    return '''<form method="post">
      <input name="name" maxlength="64" placeholder="name">
      <input name="msg" maxlength="500" placeholder="one-line message">
      <button>Leave it</button></form>'''


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)

검증하는 법: ① /write에서 글을 남기면 /에 나타나는가. ② 메시지에 <script>alert(1)</script>를 넣어도 /에서는 글자로만 보이고 실행되지 않는가(escape 확인). ③ 취약 버전 /echo에서는 같은 입력이 실행되는가 — 대조 캡처 두 장이 확보됐는가. ④ 서버 콘솔에 GET/POST 로그가 시간 순으로 찍혔는가. 전부 ‘예’이면 완성입니다.

연습문제 해답

문제 1 해답. “/hello라는 주소로 GET 요청이 오면, 바로 아래 함수를 실행하고 그 반환값을 응답 본문으로 돌려줘라”라는 연결(라우팅) 선언입니다. 주소와 함수를 잇는 것이 웹 서버의 본질이고, Flask는 그것을 데코레이터 한 줄로 표현합니다.

답 2: request.args는 메서드와 무관하게 쿼리를, request.form은 지원하는 폼 데이터를 읽습니다. URL은 기록·로그에 남을 수 있어 비밀값을 넣지 않습니다. POST 자체는 암호화하지 않고 본문도 기록될 수 있으므로 전송·끝점 보호는 별개입니다.

답 3: 이 경로에는 POST를 허용하지 않아 POST 요청이 405를 반환합니다. Flask 경로는 기본 GET 외에 보통 HEAD와 자동 OPTIONS 처리도 제공합니다. 의도한 폼 동작에 맞게 methods를 지정하세요.

답 4: escape는 여기서 쓰는 HTML 텍스트 맥락의 특수 문자를 인코딩합니다. JavaScript, CSS, URL 또는 이후 디코딩·재해석까지 보장하지는 않습니다. 실제 출력 맥락에 맞는 인코딩과 검증을 선택하세요. SQL 바인딩은 식별자가 아니라 값을 분리해 전달하는 별도 방식입니다.

완료 기준 체크리스트

  • [ ] Flask로 라우트 2개 이상의 서버를 띄울 수 있다
  • [ ] 쿼리 문자열과 폼 POST를 각각 받아 처리할 수 있다
  • [ ] 딕셔너리 반환으로 JSON 응답을 만들 수 있다
  • [ ] 브라우저 Network 탭과 서버 로그를 대조해 요청을 추적할 수 있다
  • [ ] XSS가 발생하는 코드와 그 원리(코드·데이터 혼합)를 설명할 수 있다
  • [ ] escape로 XSS를 차단하는 것을 시연할 수 있다
  • [ ] 미션: 미니 방명록과 성공/차단 대조 캡처를 완성했다

6. 흔한 실수와 해결

벽 1. 코드를 고쳤는데 브라우저가 그대로예요

증상: 분명 return 문을 바꿨는데 화면이 안 바뀝니다.

원인: 서버가 옛날 코드로 떠 있습니다. debug=True 없이 실행했거나, 재시작 감지가 실패한 경우입니다.

파일을 저장한 뒤 Ctrl+C로 실습 서버를 멈추고 python app.py를 다시 실행하세요. debug=False를 유지합니다. 실행한 프로세스가 불러온 파일과 편집 중인 파일이 같은지도 확인하세요.

벽 2. “Address already in use” 오류가 나요

증상 (출력 예시 — 환경에 따라 메시지가 다릅니다): 서버가 안 켜지며 포트가 사용 중이라고 합니다.

원인: 이전에 띄운 서버가 아직 살아 있습니다(다른 터미널 창 등).

해결: 그 창을 찾아 Ctrl+C. 못 찾겠으면 포트를 5001로 바꿔 임시 회피하세요. 실습이 끝나면 띄운 서버를 반드시 종료하는 습관을 들이세요.

벽 3. 폼을 전송하면 “Method Not Allowed”가 나와요

증상 (2026-09-09 실측): 전송 버튼을 누르면 405 Method Not Allowed.

경로가 POST를 허용하지 않았습니다. 둘 다 필요하면 methods=[“GET”, “POST”]를 선언하세요. 기본 GET 처리에는 보통 HEAD와 자동 OPTIONS도 포함됩니다.

해결: 데코레이터에 methods를 추가하세요. 그리고 폼 태그가 <form method="post">인지도 확인 — 이 둘은 세트입니다.

벽 4. from flask import escape가 ImportError가 나요

증상 (2026-09-09 실측, Flask 3.1.3):

ImportError: cannot import name 'escape' from 'flask'

원인: escape는 Flask 2.3에서 제거됐습니다. 예전 자료를 따라 치면 이 벽을 만납니다.

해결: from markupsafe import escape로 가져오세요. Flask가 내부에서 쓰는 안전 처리 도구가 바로 그것입니다.

벽 5. 한글 입력이 주소창에서 %EA%B8%B8처럼 변해요

증상: 주소창에 한글을 넣으면 %와 숫자·알파벳의 나열로 바뀝니다.

원인: 오류가 아닙니다 — URL은 영문·숫자·일부 기호만 담을 수 있어, 나머지는 % 인코딩(퍼센트 인코딩)으로 변환됩니다. 서버는 알아서 해독합니다 (실측에서도 name=길동이 서버에서 한글로 잘 꺼내졌습니다).

해결: 그대로 두면 됩니다. 서버가 받은 값을 확인하고 싶으면 라우트 안에 print(name)을 넣어 콘솔에서 보세요.


7. 정리

오늘의 개념

개념 한 줄 설명
라우팅 URL과 함수를 연결하는 선언 — 웹 서버의 본질
GET / POST 표현 조회 / 처리 요청이며 쿼리·본문 위치는 별도 속성
쿼리 문자열 ?name=값 — URL 뒤에 붙는 파라미터
상태 코드 200 성공 / 404 없는 주소 / 405 허용 안 된 메서드
XSS 입력이 HTML 문법으로 실행되는 취약점 — 코드·데이터 혼합
이스케이프 < → &lt;처럼 실행 불가 문자로 바꾸는 출력 방어
개발용/운영용 서버 app.run()은 개발 전용 — 실서비스는 별도 서버로

오늘의 코드

코드 하는 일
@app.route("/주소") 주소와 함수 연결 (라우팅)
methods=["GET", "POST"] 허용할 메서드 지정
request.args.get("이름", 기본값) 메서드와 무관하게 URL 쿼리 매개변수 읽기
request.form.get("이름") 지원하는 폼 인코딩 요청 데이터 읽기이며 임의 JSON은 아님
return 딕셔너리 JSON 응답 자동 생성
escape(문자열) HTML 텍스트용 인코딩이며 모든 맥락의 범용 XSS 방어는 아님
app.run(host="127.0.0.1", port=5000) localhost 개발 서버 기동

명령어보다 중요한 감각

서버를 직접 짜 본 오늘부터, 여러분은 웹을 두 얼굴로 봅니다. 주소창의 파라미터 하나가 request.args 한 줄에 도착하는 것을 알고, 그 값이 escape 없이 HTML에 실리면 무슨 일이 벌어지는지 압니다. 서버에 도착하는 입력은 전부 오염되었을 수 있다 — 이 문장이 웹 보안의 제1원리입니다.

그리고 프레임워크가 바뀌어도 뼈대는 안 바뀝니다. Django든 FastAPI든 라우팅, 요청 객체, GET/POST는 그대로입니다 — HTTP가 안 바뀌니까요. 오늘 만든 다섯 줄짜리 서버가, 모든 웹 서비스의 축소 모형입니다.


전부 체크되면 Step 94 완료입니다.

한 단계 더 가까이

수업을 마쳤나요?

본문의 완료 기준을 확인한 뒤 직접 표시하세요.

화면이나 링크에 문제가 있나요? 알려주세요.
다음 수업