CS · 기본
비전공자를 위한 컴퓨터공학 지도 - 전공 4년을 개발자 눈으로 따라잡기
보안 기초 - 해시·암호화·인증·흔한 취약점
해시와 암호화 차이, 비밀번호 저장, 세션과 토큰, SQL 주입·XSS 원리, 완성 코드는 hashlib 솔트 해시와 매개변수 쿼리 비교
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 요구사항을 정리하고 테스트로 동작을 확인했다면, 이번에는 그 동작을 누가 어떤 입력으로 이용할 수 있는지 살펴본다. 로그인 기능이 정상적으로 작동해도 비밀번호가 그대로 저장되거나, 입력한 문장이 데이터베이스 명령으로 해석되면 보호해야 할 정보가 노출될 수 있다. 보안은 기능 옆에 붙이는 옵션이 아니라 입력과 권한을 다루는 설계의 일부다.
다른 전공에서 개발로 옮겨 왔다면 보안을 별도의 도구 목록으로 접했을 수 있다. 전공 수업에서는 정보보호 또는 컴퓨터 보안이라는 이름 아래 데이터의 변환, 사용자 확인, 입력의 해석을 연결해서 배운다. 이 장에서는 그 연결을 작은 실험으로 확인한다. 실제 서비스를 위한 암호 도구를 직접 구현하지 않고, 표준 라이브러리로 관찰할 수 있는 경계에 집중한다.
- 해시와 암호화의 목적을 구분하고, 비밀번호에 일반 해시만 적용하면 부족한 이유를 설명한다.
- 솔트와 비밀번호 기반 키 유도 함수를 사용해 저장값을 만들고 입력한 비밀번호를 검증한다.
- 로그인 상태인 세션과 그 상태를 가리키는 토큰의 관계를 추적한다.
- SQL 주입과 XSS에서 데이터가 명령으로 해석되는 과정을 확인한다.
- 면접 질문을 기능 이름 대신 보호 대상과 실패 조건으로 풀어 설명한다.
문제 상황
전공별 학습 기록을 저장하는 작은 서비스를 만든다고 하자. 사용자는 계정을 만들고, 자료구조나 운영체제 같은 과목에 대한 자기평가를 작성한다. 작성자는 자기 기록을 수정하고 다른 사람은 공개된 소개만 읽는다. 화면과 저장 기능은 모두 동작하지만, 코드 검토에서 세 가지 문제가 발견된다.
첫째, 회원 테이블에 비밀번호가 문자열 그대로 들어 있다. 둘째, 사용자가 입력한 계정 이름을 SQL 문장에 이어 붙여 조회한다. 셋째, 자기소개를 읽기 화면의 HTML에 그대로 끼워 넣는다. 정상적인 계정 이름과 평범한 소개 문장으로만 테스트하면 세 문제 모두 드러나지 않는다.
여기에 로그인 상태를 나타내는 값도 점검해야 한다. 요청에 사용자 이름이 들어 있다는 이유만으로 수정 권한을 주면, 다른 사람의 이름을 넣은 요청도 통과할 수 있다. 보안을 살필 때는 “기능이 되는가”에 더해 “입력을 누가 만들 수 있는가”, “어떤 규칙으로 해석되는가”, “서버가 무엇을 근거로 허용하는가”를 묻는다.
완성 코드에서는 두 회원과 학습 과목을 메모리 안의 데이터베이스에 넣는다. 비밀번호 저장 실험, 로그인 상태 실험, 잘못 만든 조회와 매개변수 조회, 소개 문장의 출력 변환을 차례로 실행한다. 외부 서버나 브라우저는 필요하지 않으며 프로그램이 끝나면 데이터베이스도 사라진다.
해시와 암호화, 비밀번호 저장의 경계
원래 내용을 읽을 수 있는 데이터를 평문이라고 한다. 해시 함수(hash function)는 입력을 정해진 길이의 요약값으로 바꾼다. 같은 입력에는 같은 결과가 나오지만 결과에서 원문을 되찾는 복호화 기능은 제공하지 않는다. 전공 수업에서는 이런 결과를 해시값 또는 다이제스트라고 부른다. 다만 원문 후보를 하나씩 넣고 결과를 비교하는 추측까지 막아 주는 것은 아니다.
암호화(encryption)는 키라는 비밀값을 이용해 평문을 암호문으로 바꾸고, 정해진 키로 원문을 복원하는 과정이다. 전공 수업에서 이 복원 과정을 복호화라고 부른다. 데이터를 나중에 다시 읽어야 한다면 암호화가 필요할 수 있다. 비밀번호는 원문을 다시 읽는 대신, 사용자가 제출한 값이 맞는지 확인할 수 있으면 된다.
| 구분 | 주된 목적 | 원문 복원 | 이 장의 적용 |
|---|---|---|---|
| 일반 해시 | 내용의 요약과 비교 | 복호화 기능 없음 | 입력 변화 관찰 |
| 비밀번호용 유도 함수 | 추측 비용을 높인 검증값 생성 | 복호화 기능 없음 | 비밀번호 저장과 검증 |
| 암호화 | 키가 있는 쪽만 내용을 읽게 함 | 정해진 키로 가능 | 복원이 필요한 정보 보호 |
해시값이 같다고 해서 원문이 반드시 같다는 뜻은 아니다. 서로 다른 입력이 같은 결과를 내는 경우를 충돌이라고 한다. 다만 비밀번호 저장에서 먼저 만나는 문제는 공격자가 흔한 비밀번호 후보를 빠르게 계산해 대조할 수 있다는 점이다. 일반적인 SHA-256은 빠른 요약에 적합하며, 그 속도는 비밀번호 추측에도 유리하게 작용한다.
솔트(salt)는 비밀번호마다 따로 생성해 함께 처리하는 값이다. 같은 비밀번호라도 솔트가 다르면 저장값이 달라진다. 따라서 사용자들이 같은 비밀번호를 쓰는지 저장값만 보고 묶기 어려워지고, 여러 계정에 공통으로 쓸 미리 계산한 표의 효용도 줄어든다. 솔트는 비밀이 아니며 검증할 때 필요하므로 저장값 옆에 보관한다.
솔트만 붙인 빠른 해시도 개별 후보를 빠르게 추측할 수 있다. 그래서 비밀번호 기반 키 유도 함수(password-based key derivation function)를 사용한다. 이는 비밀번호와 솔트를 받아 계산 비용을 조절하며 결과를 만드는 함수다. 완성 코드의 PBKDF2-HMAC-SHA256은 hashlib이 제공하는 방식이다. 여기서 HMAC은 해시와 키를 결합하는 구성 방식이며 PBKDF2 내부에서 사용된다. 그 내부 설계를 직접 작성할 필요는 없다.
저장할 항목은 알고리즘 이름, 계산 비용, 솔트, 검증값이다. 로그인 때는 저장된 설정으로 제출된 비밀번호를 다시 처리하고 결과를 비교한다. 비밀번호를 복호화하는 과정은 없다. 비교에는 hmac.compare_digest를 사용한다. 이는 비교 도중 어느 위치에서 차이가 났는지에 따라 처리 시간이 달라지는 문제를 줄이기 위한 함수다.
실험에서는 출력 재현을 위해 고정 솔트를 쓴다. 실제 계정에는 계정마다 보안용 난수로 새 솔트를 만들어야 한다. 난수의 시작값을 고정하는 방법은 재현에는 유용하지만 비밀값 생성에는 적합하지 않다. 이 프로그램에는 난수 호출 자체가 없으므로 실행할 때마다 같은 결과가 나온다. 반복 횟수 역시 실험용 값이며 실제 적용값은 사용하는 저장 방식의 권고와 서버의 처리 여건을 확인해 정한다.
Python 표준 라이브러리는 여기서 사용할 범용 데이터 암호화 API를 제공하지 않는다. 따라서 되돌릴 수 있는 장난감 변환을 암호화 구현처럼 넣지 않는다. 실행 코드에서는 해시의 동일 입력 비교와 비밀번호 검증을 관찰하고, 암호화와의 차이는 원문 복원이 필요한지라는 기준으로 판단한다. 실제 암호화에는 검증된 구현과 키 관리가 함께 필요하다.
세션과 토큰은 로그인 상태를 연결한다
인증(authentication)은 요청한 사람이 누구인지 확인하는 과정이다. 인가(authorization)는 확인한 사용자에게 특정 작업을 허용할지 결정하는 과정이다. 비밀번호가 맞아서 로그인했더라도 다른 사람의 비공개 학습 기록을 수정할 권한까지 얻는 것은 아니다. 두 판단은 요청을 처리하는 서로 다른 단계다.
로그인 성공 후 관련 상태를 이어 가는 단위를 세션(session)이라고 한다. 토큰(token)은 요청자가 제시하는 증표다. 서버에 세션을 저장하고 임의의 세션 식별자를 토큰으로 건네는 방식에서는, 다음 요청의 토큰을 서버가 조회해 사용자를 찾는다. 따라서 세션과 토큰을 서로 반대되는 방식이라고만 설명하면 관계를 놓치게 된다.
완성 코드의 사전은 서버의 세션 저장소를 대신한다. 토큰을 키로, 사용자 이름을 값으로 저장한다. 올바른 토큰으로 자기 기록을 수정하면 허용하고, 같은 토큰으로 다른 사람의 기록을 수정하려 하면 거부한다. 로그아웃 때 사전에서 항목을 제거하면 그 토큰을 다시 제출해도 사용자를 찾을 수 없다.
실제 서비스의 토큰은 추측하기 어려워야 하며 만료 시간도 있어야 한다. 실험의 고정 문자열은 조회 관계를 보여 주기 위한 값이다. 토큰을 가진 쪽을 사용자로 취급하는 방식에서는 토큰 유출도 계정 오용으로 이어질 수 있다. 요청 로그나 공개 주소에 토큰을 남기지 않고, 전송 경로와 브라우저 저장 방식도 함께 검토한다.
모든 토큰이 서버 사전의 키인 것은 아니다. 토큰 안에 정보를 담고 서버가 변조 여부를 검증하는 설계도 있다. 이 경우에도 토큰을 읽을 수 있는지와 내용을 바꿀 수 있는지는 별개의 문제다. 검증용 서명이 있다고 내용까지 숨겨지는 것은 아니다. 이 장의 실험은 흐름을 명확히 볼 수 있는 서버 저장 방식만 구현한다.
입력이 명령으로 바뀌는 두 지점
SQL은 데이터베이스에 조회나 변경을 요청하는 언어다. SQL 주입(SQL injection)은 외부 입력이 SQL 문장 구조에 끼어들어 원래 의도와 다른 명령이 되는 문제다. 계정 이름 자리에 따옴표와 조건식을 넣었는데, 서버가 그 입력을 문자열 연결로 SQL에 붙이면 데이터였던 내용이 조건의 일부가 될 수 있다.
실험 입력은 작은따옴표로 문자열을 닫고, 항상 참인 조건을 덧붙이며, 뒤에 남은 문장을 주석 처리한다. 이 입력을 이어 붙인 조회는 두 회원을 모두 반환한다. 반면 매개변수 쿼리는 SQL 구조와 값을 별도로 전달한다. 물음표 자리에 들어가는 입력은 계정 이름 하나로 취급되어 일치하는 행이 없게 된다. 매개변수화는 따옴표 몇 개를 지우는 필터와 원리가 다르다.
교차 사이트 스크립팅(cross-site scripting), 줄여서 XSS는 신뢰하지 않는 내용이 웹 문서에 섞여 브라우저의 실행 내용으로 해석되는 문제다. 학습 소개에 입력한 태그를 다른 사용자의 화면에 그대로 넣으면, 그 사용자가 방문한 서비스의 문맥에서 코드가 실행될 수 있다. 이름에 사이트가 들어 있지만 별도 사이트로 이동해야만 발생하는 문제는 아니다.
HTML 본문에 일반 텍스트를 표시할 때는 특수 문자를 문자 참조로 바꾸는 이스케이프(escaping)가 필요하다. 코드의 html.escape는 꺾쇠와 따옴표 등을 바꾸어 입력을 태그가 아닌 텍스트로 표시할 수 있게 한다. 프로그램은 브라우저를 실행하지 않고 변환 전후 문자열을 출력한다. 실제 실행 여부를 재현하는 대신, HTML 해석에 영향을 주는 문자가 어떻게 달라지는지 확인한다.
단, 이 변환은 모든 출력 위치의 해결책이 아니다. 자바스크립트 코드 안이나 주소 값에 데이터를 넣으면 해당 위치의 규칙을 따로 고려해야 한다. 허용된 HTML 서식을 받아야 하는 기능도 단순한 텍스트 출력과 요구사항이 다르다. 이 장의 소개는 HTML 본문에 표시하는 일반 텍스트로 한정한다.
완성 코드
다음 내용을 main.py로 저장한다. 솔트와 토큰은 실험 전용 고정값이다. 데이터베이스는 메모리에만 만들며, SQL 주입 실험은 이 데이터베이스의 두 행을 읽는 데 그친다. 외부 패키지나 네트워크 연결은 사용하지 않는다.
import hashlib
import hmac
import html
import sqlite3
ITERATIONS = 100_000
def salted_sha256(password, salt):
return hashlib.sha256(salt + password.encode("utf-8")).digest()
def derive_password(password, salt, iterations):
return hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), salt, iterations
)
def make_record(password, salt):
return {
"algorithm": "pbkdf2-sha256",
"iterations": ITERATIONS,
"salt": salt,
"digest": derive_password(password, salt, ITERATIONS),
}
def verify_password(password, record):
if record["algorithm"] != "pbkdf2-sha256":
raise ValueError("지원하지 않는 저장 방식")
candidate = derive_password(
password, record["salt"], record["iterations"]
)
return hmac.compare_digest(candidate, record["digest"])
def can_edit(sessions, token, owner):
user = sessions.get(token)
return user is not None and user == owner
def main():
password = "study-map"
salt_a = b"demo-salt-user-a"
salt_b = b"demo-salt-user-b"
first = salted_sha256(password, salt_a)
repeated = salted_sha256(password, salt_a)
another = salted_sha256(password, salt_b)
print("같은 입력과 솔트:", first == repeated)
print("다른 솔트의 저장값:", first == another)
record = make_record(password, salt_a)
print("올바른 비밀번호:", verify_password("study-map", record))
print("다른 비밀번호:", verify_password("study-mop", record))
sessions = {}
token = "demo-session-token"
if verify_password(password, record):
sessions[token] = "learner_a"
print("자기 기록 수정:", can_edit(sessions, token, "learner_a"))
print("타인 기록 수정:", can_edit(sessions, token, "learner_b"))
sessions.pop(token, None)
print("로그아웃 뒤 수정:", can_edit(sessions, token, "learner_a"))
conn = sqlite3.connect(":memory:")
try:
conn.execute(
"CREATE TABLE learners (name TEXT PRIMARY KEY, subject TEXT)"
)
conn.executemany(
"INSERT INTO learners VALUES (?, ?)",
[
("learner_a", "자료구조"),
("learner_b", "운영체제"),
],
)
attack = "' OR 1=1 --"
unsafe_sql = (
"SELECT name FROM learners WHERE name = '" + attack + "'"
)
unsafe_rows = conn.execute(unsafe_sql).fetchall()
safe_rows = conn.execute(
"SELECT name FROM learners WHERE name = ?", (attack,)
).fetchall()
normal_rows = conn.execute(
"SELECT subject FROM learners WHERE name = ?", ("learner_a",)
).fetchall()
print("문자열 연결 조회 수:", len(unsafe_rows))
print("매개변수 조회 수:", len(safe_rows))
print("정상 조회 과목:", normal_rows[0][0])
finally:
conn.close()
introduction = '<script>alert("소개")</script>'
print("변환 전 소개:", introduction)
print("본문용 변환:", html.escape(introduction, quote=True))
if __name__ == "__main__":
main()
줄별 해설
첫 네 줄은 요약값 계산, 비교, HTML 문자 변환, 데이터베이스 사용에 필요한 모듈을 불러온다. ITERATIONS는 PBKDF2의 반복 횟수다. 이름을 대문자로 쓴 것은 프로그램 안에서 고정 설정으로 취급한다는 관례이며, Python이 값 변경을 막아 주는 것은 아니다.
salted_sha256 함수는 문자열인 비밀번호를 UTF-8 바이트로 바꾼다. 바이트는 문자 자체가 아니라 실제 계산에 넣을 수 있는 수의 나열이다. 솔트도 바이트이므로 앞에 이어 붙인 뒤 SHA-256을 계산한다. digest는 결과를 바이트로 돌려준다. 이 함수는 솔트 효과를 관찰하기 위한 비교 대상이며 실제 비밀번호 레코드에는 사용하지 않는다.
derive_password는 hashlib.pbkdf2_hmac에 해시 종류, 비밀번호 바이트, 솔트, 반복 횟수를 전달한다. make_record는 그 결과와 검증에 필요한 설정을 사전에 모은다. 예제의 사전은 메모리에 있으며, 실제 저장소에 넣을 때는 바이트를 저장할 열이나 명시적인 표현 형식이 필요하다.
verify_password는 먼저 저장 방식이 지원되는지 확인한다. 이어서 제출된 비밀번호와 레코드의 솔트 및 비용으로 후보 결과를 만든다. 마지막 줄에서 두 결과를 비교한다. 저장된 검증값과 제출된 비밀번호 문자열을 직접 비교하지 않는다는 점이 핵심이다.
can_edit는 토큰으로 사용자를 조회한다. 조회 실패는 None이므로 먼저 로그인 상태인지 확인하고, 이어서 사용자가 소유자인지 비교한다. 이 예제에서는 소유자만 수정할 수 있다는 단순한 정책을 쓴다. 공동 편집이나 관리자 기능이 있다면 허용 규칙도 달라져야 한다.
main의 처음에는 동일한 비밀번호에 두 솔트를 적용한다. 첫 비교는 동일 입력의 재현성을, 둘째 비교는 솔트에 따른 결과 차이를 보여 준다. 이어지는 두 검증은 저장 과정과 로그인 과정이 같은 함수를 사용한다는 점을 확인한다. 해시값 자체를 길게 출력하는 대신 비교 결과를 출력해 관찰 대상을 분명히 한다.
sessions에 토큰을 등록하는 부분은 로그인 성공을 나타낸다. 두 번의 수정 확인으로 인증과 인가를 구분하고, pop으로 세션을 지운 다음에는 같은 토큰도 사용할 수 없음을 확인한다. 세션 만료 시간과 브라우저 전달 과정은 생략했으므로 이 부분을 서비스의 로그인 구현 전체로 사용해서는 안 된다.
sqlite3.connect의 특수한 이름은 메모리 데이터베이스를 뜻한다. execute는 문장 하나를 실행하며 executemany는 같은 문장에 여러 값 묶음을 적용한다. fetchall은 조회 결과를 행 목록으로 가져온다. 잘못 만든 조회와 올바른 조회가 같은 attack을 사용하므로 결과 차이는 입력의 차이가 아니라 전달 방식의 차이다.
매개변수 뒤의 (attack,)은 원소가 하나인 튜플이다. 괄호만 쓴 (attack)은 문자열과 같으므로 쉼표가 필요하다. 정상 계정 조회까지 수행해 매개변수화가 조회 기능을 유지하는 것도 확인한다. finally 안의 close는 조회 중 오류가 나더라도 연결을 닫도록 한다.
마지막 두 출력은 소개 문자열과 HTML 본문용 변환 결과다. quote=True는 따옴표도 바꾸도록 지정한다. 파일 끝의 조건은 이 파일을 직접 실행했을 때 main을 호출한다. 전체 코드는 실행 후 입력을 기다리지 않고 끝난다.
실행 결과
터미널에서 파일을 저장한 디렉터리로 이동한 뒤 실행한다. 별도 컴파일 명령은 필요하지 않다. Python이 코드를 읽어 실행하며, 다음 출력에는 솔트나 토큰을 무작위로 생성하는 부분이 없으므로 실행마다 결과가 같다.
python3 main.py
같은 입력과 솔트: True
다른 솔트의 저장값: False
올바른 비밀번호: True
다른 비밀번호: False
자기 기록 수정: True
타인 기록 수정: False
로그아웃 뒤 수정: False
문자열 연결 조회 수: 2
매개변수 조회 수: 0
정상 조회 과목: 자료구조
변환 전 소개: <script>alert("소개")</script>
본문용 변환: <script>alert("소개")</script>
문자열 연결 조회의 2는 입력이 원래의 계정 이름 비교를 벗어났다는 증거다. 매개변수 조회의 0은 공격 문자열 전체와 같은 이름의 회원이 없다는 뜻이다. 본문용 변환 출력의 <는 터미널에 표시되는 문자 참조이며, 이를 HTML 본문에 넣으면 브라우저는 꺾쇠를 문자로 표시한다. 이 출력만으로 브라우저와 관련된 모든 보안 설정을 검증한 것은 아니다.
실무에서 자주 틀리는 것
솔트가 있으니 빠른 해시도 충분하다고 생각한다
다음은 솔트를 사용하지만 후보 하나를 시험하는 비용은 여전히 작다. 솔트는 계정 간 결과의 재사용을 어렵게 하고, 비밀번호용 유도 함수는 후보를 계산하는 비용을 높인다. 두 역할을 함께 봐야 한다.
# 틀린 코드: 비밀번호 저장을 일반 해시로 끝낸다.
stored = salted_sha256(password, salt_a)
# 고친 코드: 완성 코드의 비밀번호 저장 방식을 사용한다.
stored_record = make_record(password, salt_a)
matched = verify_password(password, stored_record)
위 수정도 고정 솔트를 실제 계정에 쓰라는 뜻은 아니다. 실제 생성에는 표준 라이브러리의 secrets.token_bytes(16)처럼 보안용 난수 함수를 사용할 수 있다. 이 값은 생성 후 레코드에 저장해 다음 검증에서 재사용한다. 로그인할 때마다 새 솔트를 만들면 저장된 결과와 비교할 수 없다.
사용자 이름이 요청에 있으면 권한을 준다
요청자가 적어 보낸 이름은 확인된 신원이 아니다. 아래에서 requested_name은 요청자가 마음대로 바꿀 수 있는 값이라고 가정한다. 서버가 확인한 세션 사용자와 대상 소유자를 비교해야 한다.
# 틀린 코드
allowed = requested_name == owner
# 고친 코드: 서버 세션에서 사용자를 찾는다.
allowed = can_edit(sessions, token, owner)
실제 소유자도 수정 요청의 주장만 믿지 않고 서버에 저장된 기록에서 가져와야 한다. 완성 코드의 타인 기록 수정 결과가 False인 이유는 유효한 로그인 상태와 수정 허용을 따로 판단하기 때문이다.
따옴표를 지우거나 문자열 형식만 바꿔 SQL을 만든다
문자열 보간 문법으로 바꿔도 입력이 SQL 구조에 섞이는 문제는 같다. 값은 매개변수로 전달한다. 다만 테이블 이름이나 정렬 방향처럼 SQL 구조 자체를 고르는 자리는 값 매개변수로 대체할 수 없으므로, 프로그램이 허용한 선택지에 대응시키는 방식이 필요하다.
# 틀린 코드
rows = conn.execute(
f"SELECT name FROM learners WHERE name = '{attack}'"
).fetchall()
# 고친 코드
rows = conn.execute(
"SELECT name FROM learners WHERE name = ?", (attack,)
).fetchall()
입력한 소개를 HTML 본문에 그대로 붙인다
소개를 문자열로 저장했다고 해서 출력할 때도 텍스트로 해석되는 것은 아니다. 다음 수정은 일반 텍스트를 HTML 본문에 표시한다는 조건에서 적용한다. 변환한 값을 다시 변환하면 문자 참조 자체가 화면에 보일 수 있으므로 출력 경계에서 한 번 적용하도록 흐름을 정한다.
# 틀린 코드
page = "<p>" + introduction + "</p>"
# 고친 코드
page = "<p>" + html.escape(introduction, quote=True) + "</p>"
웹 도구가 이미 자동으로 이스케이프한다면 그 기능을 유지하는 편이 좋다. 자동 변환을 끄는 기능은 입력의 출처와 출력 위치를 확인한 뒤 제한적으로 사용한다. SQL용 처리와 HTML용 처리를 하나의 공통 정리 함수로 합치지 않는다.
한눈에 보기
| 핵심 | 실무에서 쓰는 곳 | 면접에서 묻는 방식 | 설명할 근거 |
|---|---|---|---|
| 해시와 암호화 구분 | 검증값 저장과 복원이 필요한 정보 보호 | 비밀번호를 왜 암호화해 보관하지 않는가 | 로그인은 원문 복원 대신 후보 검증이 필요하다 |
| 비밀번호 저장 | 가입과 로그인 | 솔트가 있으면 무엇이 달라지는가 | 계정별 결과 분리와 추측 비용을 구분한다 |
| 인증과 인가 | 로그인 유지와 기록 수정 | 로그인했는데 왜 접근을 거부하는가 | 확인된 사용자와 대상 소유자를 비교한다 |
| 입력의 해석 경계 | 데이터 조회와 소개 출력 | 매개변수화와 출력 변환은 무엇을 막는가 | 각 해석기가 입력을 명령으로 읽지 않게 한다 |
면접에서는 “해시는 단방향이다”라는 한 문장 뒤에 멈추지 않는 편이 좋다. 원문 후보를 대입해 비교할 수 있다는 점, 솔트만으로 후보 계산이 느려지지 않는다는 점, 저장 설정을 함께 보관해야 한다는 점까지 연결하면 설계 판단을 설명할 수 있다. 세션 질문도 저장 위치만 외우기보다 발급, 조회, 권한 확인, 폐기의 흐름으로 답한다.
더 깊게 볼 서재에서는 암호학 입문서의 해시·키·메시지 인증 부분, 웹 보안 기본서의 입력 처리·세션 관리 부분, 안전한 소프트웨어 설계서의 신뢰 경계 부분을 고른다. 신뢰 경계는 외부 입력을 내부에서 믿고 처리하기 전에 확인해야 하는 지점을 뜻한다. 책을 펼쳤을 때 알고리즘 목록뿐 아니라 공격 가정, 키 관리, 실패 사례를 함께 설명하는지 확인하면 읽을 범위를 정하기 쉽다.
구현 사실을 더 확인할 때는 hashlib의 비밀번호 유도 함수 안내, sqlite3의 매개변수 전달 안내, html의 문자 변환 안내를 참고할 수 있다. 이 장의 코드와 설명은 세 기능의 경계를 비교하도록 새로 구성한 실험이다.
연습 문제
- salt_b를 salt_a와 같은 값으로 바꿔 실행한다. 바뀌는 출력 한 줄을 적고, 실제 가입 처리에서 같은 솔트를 모든 사용자에게 쓰면 어떤 이점이 줄어드는지 설명한다.
- 로그아웃 직전 존재하지 않는 토큰으로 learner_a의 기록 수정을 요청하는 출력을 추가한다. 예상 결과와, 유효한 토큰으로 타인 기록 수정을 요청할 때의 차이를 설명한다.
- attack을 learner_b로 바꿔 실행한다. 두 조회 수와 정상 조회 과목을 예상한다. 이 실행만으로 문자열 연결 방식이 안전하다고 판단할 수 있는지 설명한다.
- introduction을 <em>자료구조 복습</em>으로 바꾼다. 변환 후 문자열을 적고, 소개를 일반 텍스트로 표시하는 요구사항과 일부 서식을 허용하는 요구사항이 어떻게 다른지 설명한다.
정답과 해설
- “다른 솔트의 저장값: True”로 바뀐다. 비밀번호와 솔트가 모두 같아 같은 결과가 나온다. 모든 사용자에게 공통 솔트를 쓰면 같은 비밀번호의 저장값이 같아지고 계정마다 계산을 분리하는 이점이 줄어든다. 솔트를 비밀로 숨기는 것으로 이 문제를 해결하지 않는다.
- print("없는 토큰 수정:", can_edit(sessions, "missing-token", "learner_a"))를 추가하면 False가 나온다. 존재하지 않는 토큰은 이 예제에서 인증된 사용자를 찾지 못한다. 유효한 토큰으로 타인 기록을 요청한 경우에는 사용자를 찾았지만 인가 규칙을 통과하지 못한다.
- 문자열 연결 조회 수와 매개변수 조회 수가 모두 1이 된다. 정상 조회 과목은 여전히 자료구조다. 정상적인 값 하나가 통과했다는 사실은 입력이 SQL 구조를 바꿀 수 없다는 증거가 아니다. 원래의 공격 입력으로 확인한 결과까지 함께 봐야 한다.
- 변환 결과는 <em>자료구조 복습</em>다. 일반 텍스트 요구사항에서는 태그 기호까지 글자로 보이는 것이 맞다. 일부 서식을 허용하려면 허용할 태그와 속성을 정하고 이를 처리하는 검증된 방식을 선택해야 한다. 표준 라이브러리의 문자 변환 함수만으로 임의의 HTML을 안전하게 허용하는 기능이 생기지는 않는다.
학습 기록 서비스의 보안을 검토할 때는 비밀번호 저장 방식, 로그인 상태의 근거, 작업별 권한, 입력이 해석되는 위치를 각각 표시할 수 있다. 다음 장에서는 이렇게 확인한 과목별 빈칸을 모아 학습 순서와 준비 계획으로 옮긴다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.