AI 개발 · 기본
바이브 코딩의 정석
생성 코드의 보안 - 비밀값·입력 검증·없는 패키지
API 키·비밀번호를 코드와 대화에 넣지 않기와 환경 변수, AI 가 만든 코드의 흔한 구멍(SQL 인젝션·경로 조작·검증 없는 입력), AI 가 존재하지 않는 패키지 이름을 지어내는 문제와 설치 전 확인 습관, 모델이 좋아져도 보안은 따로 확인해야 하는 이유, 완성 코드는 sqlite3 로 문자열 이어 붙인 쿼리와 매개변수 쿼리를 비교하고 파일 경로 검증 함수를 테스트한다
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 AI에게 보여 줄 맥락을 골랐다면, 이번에는 그 맥락과 생성된 코드에 어떤 정보가 들어가도 되는지 살펴본다. 혼자 쓰는 메모·할 일 관리 도구라도 비밀번호가 코드에 남거나, 입력한 문장이 쿼리의 뜻을 바꾸거나, 파일 저장 위치가 의도한 폴더를 벗어날 수 있다. 기능이 동작한다는 사실과 안전하게 동작한다는 사실은 서로 다른 증거가 필요하다.
이번 장의 프로그램은 외부 서비스에 접속하지 않는다. Python에 포함된 데이터베이스 도구로 위험한 쿼리와 고친 쿼리를 비교하고, 파일 경로와 할 일 제목을 검사한다. 일부러 문제가 있는 코드는 임시 데이터만 다루는 작은 실험 안에 둔다. 보안을 막연한 주의 사항으로 읽는 대신, 어떤 입력이 어떤 결과를 만드는지 직접 확인한다.
- API 키와 비밀번호를 소스 코드와 AI 대화에서 제외하고, 환경 변수로 전달하는 원리를 이해한다.
- SQL 인젝션과 경로 조작이 입력을 통해 프로그램의 뜻을 바꾸는 과정을 설명한다.
- 입력의 종류·길이·허용 범위를 확인하는 검증 함수를 작성한다.
- AI가 제안한 패키지의 존재와 정체를 설치 전에 확인한다.
- 실행 결과, 테스트, 변경 전후 코드 비교로 생성 코드의 보안을 확인한다.
문제 상황
메모 도구에 검색과 파일 내보내기를 붙였다고 하자. AI에게 “제목으로 메모를 검색하고, 입력한 파일 이름으로 저장해 달라”고 요청했다. 검색은 평범한 제목을 넣으면 잘 작동한다. 내보내기도 직접 입력한 파일 이름으로 시험했을 때는 문제가 없다. 이 정도만 보고 기능을 받아들이기 쉽다.
그런데 검색 함수는 입력을 SQL 문자열에 이어 붙인다. SQL은 데이터베이스에 조회나 변경을 요청하는 언어다. 파일 저장 함수는 저장 폴더와 입력한 경로를 단순히 합친다. 제목 입력에는 길이 제한이 없다. 이 세 가지 선택은 정상 입력만으로는 잘 드러나지 않는다.
사용자: 메모 제목 검색과 텍스트 파일 내보내기를 추가해 달라. 외부 패키지가 꼭 필요한지도 설명해 달라.
AI의 답 예시: 제목을 쿼리에 넣어 검색하고, 저장 폴더에 파일 이름을 붙이면 된다. 내보내기에는 가상의 패키지 이름인 memo-export-helper를 설치하면 편리하다.
이 답에는 확인할 일이 남아 있다. 제목을 “쿼리에 넣는다”는 말이 문자열 연결인지 매개변수 전달인지 불분명하다. 파일 이름이 저장 폴더 밖을 가리킬 때의 처리도 없다. 패키지 이름이 자연스럽다는 이유만으로 실제 배포되는 패키지라고 판단할 수도 없다. 위 이름은 문제를 설명하기 위한 가상의 이름이며 설치 대상이 아니다.
독자가 혼자 쓰는 프로그램이라도 입력의 출처는 넓어질 수 있다. 다른 곳에서 복사한 제목, 가져온 메모 파일, 나중에 추가한 웹 화면이 새로운 입력 경로가 된다. 지금은 직접 타이핑한다는 가정에만 기대면 기능을 키울 때 검증해야 할 지점을 놓치기 쉽다.
따라서 검토 질문을 바꾼다. “검색이 되나”에 더해 “입력이 검색 명령의 구조를 바꿀 수 있나”를 묻는다. “저장이 되나”에 더해 “저장 위치가 허용한 폴더 안인가”를 묻는다. AI에게 설명을 요구한 뒤에도 실제 코드와 실행 결과가 그 설명에 맞는지 확인한다.
비밀값은 코드와 대화 밖에 둔다
API 키는 프로그램이 외부 서비스에 접근할 때 사용하는 인증 정보다. 비밀번호와 마찬가지로 사용 권한을 나타내므로, 예제 문자열처럼 취급하면 안 된다. 소스 코드에 넣으면 파일 공유나 변경 기록을 통해 퍼질 수 있다. AI 대화에 붙여 넣으면 코드 파일에서 지웠더라도 대화에 값이 남는다.
AI에게 필요한 것은 대개 비밀값 자체가 아니라 값을 읽는 방식과 누락되었을 때의 처리다. “환경 변수 MEMO_API_KEY에서 읽는다”라는 정보면 코드를 작성하는 데 충분하다. 오류를 설명할 때도 키의 앞부분이나 뒷부분을 보여 주기보다, 값이 설정되었는지와 어떤 단계에서 실패했는지를 전달한다.
환경 변수(environment variable)는 프로그램 실행 환경에 이름과 값으로 전달되는 설정이다. Python에서는 os.environ으로 읽을 수 있다. 비밀값을 소스 코드와 분리하는 데 도움이 되지만, 환경 변수라는 이유로 값이 자동으로 보호되는 것은 아니다. 값을 출력하거나 오류 메시지에 포함하면 다시 노출된다. 실행 환경을 관리하는 방법도 별도로 살펴야 한다.
이번 완성 코드에는 인증이 필요한 기능이 없으므로 환경 변수를 읽지 않는다. 실행 환경에 따라 결과가 달라지는 요소를 넣을 필요도 없다. 환경 변수 사용 예시는 뒤의 짧은 코드로 확인하며, 실제 API 키나 네트워크 호출은 사용하지 않는다.
이 프로그램은 나중에 인증이 필요한 기능을 붙일 수 있다. 비밀값은 코드에 쓰지 않고 MEMO_API_KEY 환경 변수에서 읽는 구조로 설계하라. 값이 없으면 설정이 필요하다고 알리고, 값 자체는 로그나 예외 메시지에 넣지 마라. 지금은 외부 서비스 호출을 구현하지 마라.
이미 실제 키를 코드나 대화에 넣었다면 문자열 삭제만으로 처리를 끝내지 않는다. 노출된 키를 폐기하거나 교체하고, 사용한 서비스의 절차에 따라 접근 기록을 확인한다. 변경 기록과 로그에도 남았을 수 있기 때문이다. 이후 AI에게 전달할 자료에서는 비밀값을 가린 뒤, 가려진 상태에서도 문제를 재현할 수 있는지 확인한다.
입력은 명령과 경로의 경계를 넘지 못하게 한다
검색어를 SQL 문장에 이어 붙이지 않는다
SQL 인젝션(SQL injection)은 입력이 데이터로만 취급되지 않고 SQL 명령의 일부가 되는 문제다. 제목 검색 쿼리에 작은따옴표로 감싼 입력을 이어 붙이면, 입력에 들어 있는 작은따옴표가 문자열의 끝으로 해석될 수 있다. 이어지는 문자는 검색 조건이나 주석의 뜻을 갖게 된다.
완성 코드에서는 제목과 정확히 일치하는 메모를 찾는다. 정상 제목인 “장보기”는 한 건을 반환한다. 그러나 작은따옴표와 참인 조건을 포함한 입력을 위험한 검색 함수에 넣으면 모든 메모가 반환된다. 이 실험은 메모리 안의 작은 데이터베이스에서만 수행한다.
고친 함수는 매개변수 쿼리(parameterized query)를 사용한다. SQL 문장에는 물음표로 값의 자리를 표시하고, 실제 값은 execute의 두 번째 인수로 전달한다. sqlite3가 문장과 값을 구분하므로 검색어의 작은따옴표가 명령 구조를 바꾸지 않는다. 같은 수상한 입력은 그 문자열과 정확히 같은 제목을 찾는 값이 되어, 이번 데이터에서는 결과가 없다.
여기서 매개변수는 값의 자리에 사용한다. 테이블 이름이나 정렬 방향처럼 SQL 구조를 결정하는 부분까지 물음표로 바꿀 수 있다고 생각하면 안 된다. 그런 기능이 필요하면 프로그램이 미리 정한 선택지에서 고른다. 이번 검색에서는 구조를 고정하고 제목만 값으로 전달한다.
파일 이름을 붙이는 것과 저장 위치를 확인하는 것은 다르다
경로 조작(path traversal)은 입력한 경로로 허용한 폴더 밖의 파일에 접근하는 문제다. 경로의 두 점은 상위 폴더를 뜻한다. 저장 폴더 뒤에 “../outside.txt”를 붙였다고 해서 결과가 그 폴더 안에 머무는 것은 아니다. 절대 경로가 들어오면 폴더를 합치는 연산의 결과 자체가 바뀔 수도 있다.
이번 도구의 규칙은 명확하게 정한다. 상대 경로만 받고, 상위 폴더로 이동하는 요소는 거부한다. 실제 경로를 계산한 뒤 저장 폴더 안인지 확인한다. 마지막으로 확장자가 .txt인지 검사한다. “notes/today.txt”처럼 하위 폴더를 나타내는 경로는 허용한다.
심볼릭 링크(symbolic link)는 다른 파일이나 폴더를 가리키는 경로다. 겉으로는 저장 폴더 안에 있는 이름이라도 실제로는 밖을 가리킬 수 있다. pathlib의 resolve는 이런 연결을 따라 실제 경로를 계산한다. 그 결과에 relative_to를 적용해 저장 폴더 안에 포함되는지 확인한다. 글자 앞부분이 같은지 비교하는 것보다 경로의 구조를 검사하는 편이 적절하다.
이 함수는 경로를 확인할 뿐 파일을 저장하지 않는다. 파일이 아직 없어도 경로를 계산할 수 있도록 strict=False를 사용한다. 다만 검사 이후 다른 프로그램이 폴더나 링크를 바꾸면 저장 시점의 경로가 달라질 수 있다. 검사와 사용 사이의 변경 문제다. 이번 실습은 다른 프로그램이 임시 폴더를 바꾸지 않는 조건에서 경로 검증을 확인한다. 여러 사용자가 함께 쓰는 서비스로 확대하면 파일을 여는 방식과 폴더 권한까지 다시 검토해야 한다.
입력 검증은 용도에 맞게 정한다
입력 검증(input validation)은 값이 프로그램에서 허용하는 조건을 만족하는지 확인하는 과정이다. 할 일 제목이라면 문자열인지, 비어 있지 않은지, 너무 길지 않은지 살핀다. 한 줄짜리 제목으로 쓰기로 했다면 줄바꿈과 제어 문자도 거부한다. 제어 문자는 줄바꿈처럼 표시나 처리 방식에 영향을 주는 문자다.
이번 예제는 제목 앞뒤 공백을 제거하고, 길이를 1자 이상 60자 이하로 제한한다. 탭·LF·CR을 포함해 문자 코드가 32 미만인 문자와 127인 문자를 거부한다. 다만 U+0085, U+2028, U+2029 같은 Unicode 줄 경계 문자는 통과하므로, 한 줄 제목을 보장하려면 추가 검사가 필요하다. 이 60자는 도구의 사용 규칙이며 다른 프로그램에 그대로 적용할 보안 기준은 아니다. 화면과 저장 방식에 맞춰 규칙을 정해야 한다.
제목 검증을 했다고 SQL 문자열 연결이 안전해지는 것은 아니다. 정상 제목에도 작은따옴표가 들어갈 수 있다. 입력 검증은 도구의 규칙을 지키고, 매개변수 쿼리는 입력과 SQL 명령을 분리한다. 각 검사가 맡는 일을 구별해야 빠진 부분을 알아볼 수 있다.
패키지 이름과 보안 설명도 확인 대상이다
AI는 익숙한 단어를 조합해 그럴듯한 패키지 이름을 제안할 수 있다. 실제로 없는 이름일 수도 있고, 비슷한 이름의 다른 패키지를 가리킬 수도 있다. 나중에 누군가 같은 이름으로 패키지를 배포하더라도, 그것이 원래 의도한 도구라는 증거는 아니다.
설치 전에는 필요한 기능이 표준 라이브러리에 있는지 먼저 살핀다. 이번 도구의 데이터 저장, 경로 처리, 임시 폴더 생성은 sqlite3, pathlib, tempfile로 처리한다. 외부 패키지를 설치할 이유가 없다. 의존성(dependency)은 프로그램이 동작하기 위해 기대는 다른 코드나 구성 요소다. 의존성이 늘면 확인하고 관리할 대상도 늘어난다.
| 확인 항목 | 살펴볼 내용 | 판단 |
|---|---|---|
| 필요성 | 표준 라이브러리로 기능을 구현할 수 있는가 | 충분하다면 설치하지 않는다 |
| 존재 | 배포 저장소에 정확한 이름이 있는가 | AI의 설명만으로 존재를 판단하지 않는다 |
| 정체 | 프로젝트 문서와 배포 페이지가 서로 연결되는가 | 이름이 비슷한 다른 프로젝트인지 확인한다 |
| 실행 영향 | 설치와 실행에 어떤 코드·권한이 필요한가 | 이해하지 못한 설치 명령을 바로 실행하지 않는다 |
패키지가 존재한다고 안전성이 보장되는 것은 아니다. 설명한 기능이 실제로 제공되는지, 유지 관리 상태와 의존성이 어떤지 추가로 살핀다. 이름을 확인하는 습관은 검토의 시작이다. 이 장에서는 네트워크에 접속하거나 패키지를 설치하지 않고, 확인해야 할 근거만 정리한다.
모델이 좋아지면 이런 문제를 먼저 지적하는 답을 받을 가능성은 높아질 수 있다. 그래도 답이 자연스럽고 기능이 정상 실행된다는 사실만으로 보안 검토를 대신할 수는 없다. AI가 저장 폴더를 누가 수정할 수 있는지 모르거나, 빠르게 만든 예시에서 경계 입력 처리를 생략할 수 있기 때문이다.
검색 함수의 문자열 연결을 매개변수 전달로 바꾸고, 경로 검사 함수에는 상위 폴더 이동·절대 경로·바깥을 가리키는 링크 테스트를 추가하라. 변경 전후 차이를 설명하라. 테스트 통과 여부는 실제 실행 결과에 근거해 보고하라.
변경 전후 비교인 diff에서는 실제로 문자열 연결이 사라졌는지 확인한다. 실행에서는 같은 입력이 어떤 결과를 만드는지 확인한다. 테스트에서는 허용할 입력과 거부할 입력을 각각 넣는다. AI가 “안전하게 수정했다”고 설명했더라도, 이 세 가지 근거가 설명과 일치하는지 독자가 판단한다.
세부 동작의 사실 확인에는 공식 문서를 사용할 수 있다. sqlite3 문서에서는 값 전달 방식을, pathlib 문서에서는 경로 계산을, os.environ 문서에서는 환경 변수 접근을 확인할 수 있다. 문서의 문장을 외우기보다 작성한 코드가 어떤 규칙에 기대는지 확인한다.
완성 코드
다음 프로그램을 main.py로 저장한다. Python 3.12 이상에서 표준 라이브러리만 사용한다. 데이터베이스는 메모리 안에 만들고 파일 경로 테스트는 임시 폴더 안에서 수행한다. 실제 메모 파일은 읽거나 쓰지 않는다. 임시 폴더 이름을 출력하지 않으므로 매번 같은 결과를 얻는다.
import sqlite3
import tempfile
from pathlib import Path
def unsafe_search(connection, title):
# 비교 실험 전용이다. 실제 검색 기능에 사용하지 않는다.
sql = "SELECT id FROM notes WHERE title = '" + title + "' ORDER BY id"
return [row[0] for row in connection.execute(sql)]
def safe_search(connection, title):
sql = "SELECT id FROM notes WHERE title = ? ORDER BY id"
return [row[0] for row in connection.execute(sql, (title,))]
def validate_title(value):
if not isinstance(value, str):
raise ValueError("제목은 문자열이어야 한다")
if any(ord(char) < 32 or ord(char) == 127 for char in value):
raise ValueError("제목에 제어 문자를 넣을 수 없다")
title = value.strip()
if not 1 <= len(title) <= 60:
raise ValueError("제목 길이는 1자 이상 60자 이하여야 한다")
return title
def validated_path(root, value):
if not isinstance(value, str) or not value or len(value) > 80:
raise ValueError("파일 경로 형식이 맞지 않는다")
if any(ord(char) < 32 or ord(char) == 127 for char in value):
raise ValueError("파일 경로에 제어 문자를 넣을 수 없다")
relative = Path(value)
if relative.is_absolute() or ".." in relative.parts:
raise ValueError("상대 경로와 하위 폴더만 허용한다")
base = root.resolve(strict=True)
target = (base / relative).resolve(strict=False)
try:
target.relative_to(base)
except ValueError:
raise ValueError("저장 폴더 밖의 경로다") from None
if target.suffix != ".txt":
raise ValueError("txt 파일만 허용한다")
return target
def check(condition, message):
if not condition:
raise AssertionError(message)
def test_titles():
cases = [
(" 장보기 ", "장보기"),
(" ", None),
("가" * 61, None),
("첫 줄\n둘째 줄", None),
(123, None),
]
for value, expected in cases:
try:
actual = validate_title(value)
except ValueError:
check(expected is None, "허용할 제목을 거부했다")
else:
check(expected is not None, "거부할 제목을 허용했다")
check(actual == expected, "제목 정리 결과가 다르다")
return len(cases)
def test_paths():
with tempfile.TemporaryDirectory() as temporary:
parent = Path(temporary)
root = parent / "storage"
outside = parent / "outside"
root.mkdir()
outside.mkdir()
(root / "link").symlink_to(outside, target_is_directory=True)
cases = [
("notes/today.txt", True),
("../outside.txt", False),
(str(outside / "secret.txt"), False),
("notes/../../outside.txt", False),
("notes/today.csv", False),
("", False),
("link/secret.txt", False),
]
for value, allowed in cases:
try:
actual = validated_path(root, value)
except ValueError:
check(not allowed, "허용할 경로를 거부했다")
else:
check(allowed, "거부할 경로를 허용했다")
expected = root.resolve() / "notes" / "today.txt"
check(actual == expected, "허용 경로의 계산 결과가 다르다")
return len(cases)
def main():
connection = sqlite3.connect(":memory:")
try:
connection.execute(
"CREATE TABLE notes (id INTEGER PRIMARY KEY, title TEXT NOT NULL)"
)
connection.executemany(
"INSERT INTO notes (id, title) VALUES (?, ?)",
[(1, "장보기"), (2, "책 읽기"), (3, "산책")],
)
suspicious = "' OR 1=1 --"
normal = safe_search(connection, "장보기")
unsafe = unsafe_search(connection, suspicious)
safe = safe_search(connection, suspicious)
check(normal == [1], "정상 검색 결과가 다르다")
check(sorted(unsafe) == [1, 2, 3], "문자열 연결 실험 결과가 다르다")
check(safe == [], "매개변수 검색 결과가 다르다")
finally:
connection.close()
title_count = test_titles()
path_count = test_paths()
print(f"정상 제목 검색: {normal}")
print(f"문자열 연결 검색: {unsafe}")
print(f"매개변수 검색: {safe}")
print("SQL 비교 검사: 3개 통과")
print(f"제목 검증: {title_count}개 통과")
print(f"경로 검증: {path_count}개 통과")
if __name__ == "__main__":
main()
줄별 해설
첫 세 줄은 데이터베이스, 임시 폴더, 경로 처리에 필요한 모듈을 가져온다. 모두 Python에 포함되어 있다. unsafe_search는 입력 문자열을 SQL 문장에 이어 붙인다. 이름과 주석에 비교 실험 전용이라는 용도를 적었다. 문제를 관찰하기 위해 남긴 함수이므로 실제 도구로 옮길 때는 제거한다.
safe_search는 쿼리의 구조를 고정하고 제목을 별도로 전달한다. (title,)의 쉼표는 값 하나가 들어 있는 튜플을 만든다. 튜플은 여러 값을 묶어 전달하는 자료형이다. 괄호만 있는 (title)은 제목 자체이므로, 쉼표를 빠뜨리지 않는다. safe_search는 ORDER BY id로 반환 순서를 고정한다. unsafe_search에서는 수상한 입력의 --가 뒤의 ORDER BY id까지 주석으로 만들어 순서를 보장하지 못한다. 문자열 연결 실험에서 반환된 번호들을 비교할 때는 sorted(unsafe) == [1, 2, 3]으로 검사한다.
validate_title의 첫 검사는 문자열이 아닌 값을 거부한다. 다음 검사는 ord로 각 문자의 숫자 값을 확인한다. 이번 규칙에서는 32보다 작은 값과 127을 제어 문자로 처리한다. strip보다 먼저 검사하므로 양끝의 줄바꿈도 조용히 없어지지 않고 거부된다. 그 뒤 공백을 정리하고 길이를 확인해 반환한다.
validated_path는 먼저 입력 종류와 길이를 확인한다. Path로 바꾼 뒤에는 절대 경로와 상위 폴더 이동 요소를 거부한다. root.resolve(strict=True)는 저장 폴더가 이미 존재해야 한다는 조건을 확인한다. 검사 대상의 resolve(strict=False)는 아직 만들지 않은 파일도 다루면서, 이미 존재하는 링크를 따라 경로를 계산한다.
target.relative_to(base)는 대상이 기준 폴더 아래에 있으면 그 아래의 경로를 계산한다. 포함되지 않으면 ValueError가 발생한다. 함수는 이를 도구에서 이해할 수 있는 거부 이유로 바꾼다. 마지막 확장자 검사는 실제로 계산된 대상이 .txt로 끝나는지 확인한다.
check는 조건이 거짓일 때 AssertionError를 발생시키는 작은 검사 함수다. 일반 assert 문과 달리 Python의 최적화 실행 설정에 따라 검사가 빠지지 않는다. 테스트는 예상한 거부만 ValueError로 받는다. 다른 종류의 오류가 나면 통과로 숨기지 않고 프로그램이 멈추게 한다.
test_titles는 정상 입력 하나와 거부할 입력 네 개를 확인한다. 정상 입력에서는 반환된 제목까지 비교한다. test_paths는 임시 폴더 안에 저장 폴더와 그 바깥 폴더를 만들고, 바깥을 가리키는 링크를 추가한다. 경로를 문자열로 볼 때와 실제 위치로 볼 때의 차이를 시험하기 위한 준비다.
main의 데이터베이스는 :memory:를 사용하므로 디스크 파일로 남지 않는다. 데이터 삽입에도 매개변수를 사용한다. 정상 검색, 문자열 연결 검색, 매개변수 검색의 결과를 각각 검사하고 finally에서 연결을 닫는다. 제목과 경로 검사까지 모두 끝난 뒤에만 통과 메시지를 출력한다.
이 코드는 SQL 비교 세 개, 제목 검사 다섯 개, 경로 검사 일곱 개의 기대 결과를 담고 있다. 이 수는 작성한 사례의 수이며 모든 위험을 확인했다는 뜻은 아니다. 기능을 추가하면 새 입력 경로와 새 저장 방식에 맞는 사례도 추가한다.
실행 결과
main.py가 있는 폴더에서 다음 명령을 실행한다.
python3 main.py
예상 출력은 다음과 같다. 테스트가 실패하면 이 출력 대신 예외가 나타난다.
정상 제목 검색: [1]
문자열 연결 검색: [1, 2, 3]
매개변수 검색: []
SQL 비교 검사: 3개 통과
제목 검증: 5개 통과
경로 검증: 7개 통과
문자열 연결 검색이 세 건을 반환하는 것이 관찰할 문제다. 검색어가 제목이라는 값에 머물지 않고 조건을 바꾸었다. 매개변수 검색은 같은 입력을 제목 그대로 취급한다. 경로 검사에서는 저장 폴더 안의 .txt 경로만 통과하고, 바깥으로 나가는 입력과 잘못된 형식은 거부된다.
실무에서 자주 틀리는 것
비밀값을 코드에 넣고 출력한다
다음은 실제 비밀값 대신 연습용 문자열을 쓴 틀린 예다. 문제는 문자열의 내용보다 값을 소스에 넣고 출력하는 방식이다.
api_key = "practice-value"
print("사용할 키:", api_key)
고친 예는 환경 변수의 값 자체를 출력하지 않는다. 환경 변수를 설정하지 않아도 바로 끝나며 네트워크에 접속하지 않는다.
import os
api_key = os.environ.get("MEMO_API_KEY")
if api_key:
print("인증 설정을 읽었다")
else:
print("인증 설정이 필요하다")
이 짧은 예의 출력은 실행 환경에 따라 달라진다. 완성 코드의 결정적인 출력과는 구별한다. AI에게 전달할 때는 변수 이름과 처리 규칙만 보여 준다.
작은따옴표를 지우면 SQL 문제가 해결된다고 생각한다
다음 코드는 따옴표를 없애면서 제목을 바꾼다. 문자열 연결 방식도 그대로 남는다.
title = "오늘의 '작은' 계획"
changed = title.replace("'", "")
sql = "SELECT id FROM notes WHERE title = '" + changed + "'"
print(changed)
print(sql)
고친 예는 제목을 그대로 보존하면서 값과 명령을 분리한다. 아래 코드 전체를 실행하면 [1]을 출력한다.
import sqlite3
connection = sqlite3.connect(":memory:")
try:
connection.execute("CREATE TABLE notes (id INTEGER, title TEXT)")
title = "오늘의 '작은' 계획"
connection.execute("INSERT INTO notes VALUES (?, ?)", (1, title))
rows = connection.execute(
"SELECT id FROM notes WHERE title = ?", (title,)
)
print([row[0] for row in rows])
finally:
connection.close()
경로의 문자열 앞부분만 비교한다
다음 코드는 저장 폴더 이름으로 시작한다는 이유로 다른 폴더까지 허용한다. 실행하면 True가 나온다.
root = "/tmp/memo"
candidate = "/tmp/memo-backup/private.txt"
print(candidate.startswith(root))
고친 예는 경로 요소를 기준으로 포함 관계를 확인한다. 실행하면 False가 나온다. 실제 저장 기능에서는 완성 코드처럼 resolve로 링크까지 확인한 뒤 이 검사를 적용한다.
from pathlib import Path
root = Path("/tmp/memo")
candidate = Path("/tmp/memo-backup/private.txt")
try:
candidate.relative_to(root)
except ValueError:
print(False)
else:
print(True)
입력이 문자열인지 확인하지 않고 정리한다
파일이나 화면에서 받은 값은 기대한 종류와 다를 수 있다. 다음 코드는 숫자를 받으면 AttributeError로 멈춘다.
def clean_title(value):
return value.strip()
try:
print(clean_title(123))
except AttributeError:
print("문자열이라고 가정해 처리에 실패했다")
고친 예는 허용 조건을 먼저 확인하고, 잘못된 입력을 일관된 예외로 알린다. 전체 제목 규칙은 완성 코드의 validate_title에 둔다.
def clean_title(value):
if not isinstance(value, str):
raise ValueError("제목은 문자열이어야 한다")
title = value.strip()
if not title:
raise ValueError("빈 제목은 허용하지 않는다")
return title
try:
print(clean_title(123))
except ValueError as error:
print(error)
한눈에 보기
| 대상 | 적용할 규칙 | 확인할 증거 |
|---|---|---|
| API 키·비밀번호 | 코드와 대화에서 제외하고 실행 환경으로 전달한다 | 값이 출력·예외·변경 기록에 없는지 살핀다 |
| SQL 검색어 | 값을 매개변수로 전달한다 | 수상한 입력도 명령 구조를 바꾸지 않는다 |
| 파일 경로 | 형식과 실제 위치를 확인한다 | 상위 이동·절대 경로·바깥 링크를 거부한다 |
| 할 일 제목 | 종류·빈 값·길이·제어 문자를 검사한다 | 허용과 거부 사례가 모두 기대대로 동작한다 |
| 외부 패키지 | 필요성과 정확한 배포 정보를 확인한다 | 문서와 배포 페이지가 같은 프로젝트를 가리킨다 |
| AI의 수정 설명 | 실제 변경과 실행 결과를 대조한다 | diff와 테스트 결과가 설명에 맞는다 |
보안 검토는 입력이 프로그램의 뜻을 바꿀 수 있는 지점을 찾는 작업이다. 비밀값은 전달 범위를 줄이고, 검색어는 명령과 분리하며, 경로는 실제 위치를 확인한다. 다음 장에서 도구를 합칠 때도 각 기능의 정상 동작과 함께 이 경계가 유지되는지 확인한다.
연습 문제
- 완성 코드의 메모에 작은따옴표가 들어 있는 제목을 추가하라. 매개변수로 삽입하고 검색해 해당 메모만 반환되는지 확인하라. 이 사례가 수상한 입력 검사와 어떻게 다른지도 설명하라.
- 경로 테스트에 “notes/today.TXT”와 “notes/today.txt/child.txt”를 추가하라. 현재 규칙에서 각각 허용되는지 예측한 뒤 실행하라. 경로에 .txt라는 글자가 있는 것과 대상 확장자가 .txt인 것이 어떻게 다른지 설명하라.
- 제목 길이 경계를 확인하도록 60자 제목과 61자 제목을 시험하라. 제어 문자 검사 사례로 탭이 들어 있는 제목도 추가하라. 기대 결과를 먼저 적고 실제 결과와 비교하라.
- AI가 메모 내보내기를 위해 새 패키지 설치를 제안했다고 가정하라. 설치 전에 확인할 항목 세 가지와, 실제 API 키 없이 AI에게 요청할 문장을 작성하라.
정답과 해설
예를 들어 번호 4와 제목 “오늘의 '작은' 계획”을 삽입 자료에 추가하고, safe_search의 결과가 [4]인지 check로 확인한다. 삽입과 조회 모두 값을 매개변수로 전달한다. 이 사례는 정상적인 작은따옴표를 보존하는지 확인한다. 수상한 입력 검사는 SQL처럼 보이는 값이 명령 구조를 바꾸지 않는지 확인한다. 두 사례를 함께 두면 잘못된 입력을 막으면서 정상 데이터도 유지하는지 살필 수 있다.
“notes/today.TXT”는 거부된다. 현재 코드는 확장자를 대소문자 구별 없이 바꾸지 않고 .txt와 직접 비교하기 때문이다. “notes/today.txt/child.txt”는 허용된다. today.txt라는 폴더 이름 아래의 child.txt를 가리키며, 최종 대상의 확장자는 .txt다. 현재 함수는 아직 없는 경로도 검사하고 폴더 이름의 점을 금지하지 않는다. 원하는 규칙이 다르면 폴더 이름 조건을 명시하고 테스트를 추가한다.
다른 문제가 없는 60자 제목은 허용되고 61자 제목은 거부된다. 탭이 들어 있는 제목은 길이가 짧아도 거부된다. 제목 길이를 한 문자만 시험하면 경계 조건을 잘못 쓴 코드를 놓칠 수 있다. 허용되는 최대 길이와 그 바로 다음 길이를 함께 확인한다.
표준 라이브러리로 충분한지, 정확한 이름의 배포가 존재하는지, 문서와 배포 페이지가 같은 프로젝트를 가리키는지 확인한다. 요청문은 다음처럼 쓸 수 있다.
메모를 텍스트 파일로 내보내는 기능을 표준 라이브러리로 먼저 검토하라. 외부 패키지가 필요하다면 필요한 이유와 확인할 공식 문서·배포 정보를 제시하라. 인증 기능은 구현하지 말고, 이후 필요할 때 사용할 환경 변수 이름만 제안하라. 비밀값을 요청하거나 출력하는 코드는 넣지 마라.
제안받은 링크와 이름은 직접 대조한다. 확인할 근거를 요청하는 것과 그 근거가 맞는지 검토하는 것은 각각 필요한 작업이다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.