파이썬의 requests를 사용하여 통신을 디버깅할 때, 단순히 logging을 DEBUG 레벨로 설정만 했다면 Authorization 헤더 값은 노출되지 않고 "GET / HTTP/1.1" 200과 같은 한 줄만 출력됩니다 (2026-11-09, Python 3.13.5・requests 2.34.2・urllib3 2.8.0, 더미 값으로 실기 확인). 그런데 http.client.HTTPConnection.debuglevel = 1을 추가하자, send: 라인에 Authorization: Bearer ...가 그대로 출력되었습니다. 또한 auth=(user, key)를 사용한 Basic 인증도 같은 라인에 나타납니다. Base64로 되어 있기 때문에 원래 문자열로 복구할 수 있습니다. 반대로, params={"api_key": ...}처럼 키를 URL에 포함시키면, debuglevel 설정 없이도 urllib3의 DEBUG 로그에 URL 단위로 노출됩니다. 이 경우 send: 라인은 logging이 아닌 곳에서 출력되며, 로그 설정으로 마스킹했다고 해도 효과가 없습니다. print() 함수를 사용해 표준 출력으로 나옵니다.
본 기사는 "AI 에이전트와 비밀 정보" 검증 시리즈입니다. 새로운 검증 내용은 X (@supportapp3_lab)에서도 공지하고 있습니다.
코드에서 debuglevel을 높이는 코드가 남아있는지 찾아봅니다.
grep -rnE --include='*.py' 'debuglevel|set_debuglevel' .
키를 URL 쿼리 파라미터에 넣은 부분도 찾을 수 있습니다 (찾더라도, 해당 API가 헤더 지정 방식과 호환되는지는 각 API 사양을 확인해야 합니다).
grep -rnE --include='*.py' "params=.*(api_?key|token)|[?&](api_?key|token)=" .
401 에러 등을 조사할 때, "통신 내용을 전부 보고 싶다"는 생각으로 debuglevel = 1을 설정하는 코드를 자주 볼 수 있습니다. 이 출력에는 전송된 헤더가 그대로 포함됩니다.
터미널에 출력되는 내용은 로그 파일이나 CI(지속적 통합) 로그, 화면 공유, AI 에이전트에 전달되는 오류 전문 등에 그대로 혼재될 수 있습니다. Codex나 Claude Code처럼 명령을 실행하고 출력을 읽는 도구에서는 그 출력 내용까지 대화 기록에 포함됩니다.
로컬에 작은 HTTP 서버를 띄우고 더미 키를 전송해 보았습니다 (외부로는 전송하지 않았습니다).
import logging, sys, http.client, requests
URL = "http://127.0.0.1:18081/"
HDR = {"Authorization": "Bearer sk-dummy-0000"}
...
(서버 부분은 생략했습니다. 전체는 http.server로 200을 반환하는 것만 포함합니다.)
urllib3.connectionpool DEBUG Starting new HTTP connection (1): 127.0.0.1:18081
urllib3.connectionpool DEBUG http://127.0.0.1:18081 "GET / HTTP/1.1" 200 None
헤더는 출력되지 않았습니다.
send: b'GET / HTTP/1.1\r\nHost: 127.0.0.1:18081\r\nUser-Agent: python-requests/2.34.2\r\nAccept-Encoding: gzip, deflate, br, zstd\r\nAccept: */*\r\nConnection: keep-alive\r\nAuthorization: Bearer sk-dummy-0000\r\n\r\n'
reply: 'HTTP/1.0 200 OK\r\n'
Authorization: Bearer sk-dummy-0000가 그대로 출력되었습니다.
send: b'GET / HTTP/1.1\r\n...\r\nAuthorization: Basic dXNlcjpzay1kdW1teS0xMTEx\r\n\r\n'
$ echo -n 'user:sk-dummy-1111' | base64
dXNlcjpzay1kdW1teS0xMTEx
Base64는 암호가 아니므로, 출력에서 그대로 user:sk-dummy-1111로 되돌릴 수 있습니다.
requests.get(URL, params={"api_key": "sk-dummy-2222"})
urllib3.connectionpool http://127.0.0.1:18082 "GET /?api_key=sk-dummy-2222 HTTP/1.1" 200 None
debuglevel을 높이지 않았더라도, DEBUG 로그의 한 줄에 URL별로 출력되었습니다.
로그 출력 위치를 표준 에러(stderr)로 설정하고, 표준 출력(stdout)과 분리하여 실행했습니다. send:
다음 줄은 표준 출력 측에 나타났습니다.
[stdout] send: b'GET / HTTP/1.1\r\n...Authorization: Bearer sk-dummy-3333\r\n\r\n'
[stderr] urllib3.connectionpool http://127.0.0.1:18082 "GET / HTTP/1.1" 200 None
제 로컬의 Python 3.13.5 http/client.py에서도 print("send:", repr(data))라고 작성되어 있었습니다. logging 필터로 마스크해도 이 줄에는 효과가 없습니다.
debuglevel = 1은 조사가 끝나면 반드시 제거합니다. 커밋 전에 위의 grep으로 확인합니다. - 아무래도 통신을 보고 싶을 때는, 더미 키나 즉시 무효화할 수 있는 임시 키로 시도합니다.
- 키는 URL의 쿼리 파라미터가 아닌, 헤더로 전달합니다 (API가 지원하는 경우). 쿼리는 DEBUG 로그에 URL별로 출력되었습니다.
- 디버그 출력을 AI에 붙여넣거나 로그로 저장하기 전에,
Authorization나 api_key와 같은 줄이 없는지 확인합니다. - AI에게 디버깅을 요청할 때는 "debuglevel을 사용하지 않기", "키를 로그에 출력하지 않기" 등의 조건을 추가합니다.
ai-secret-check scan은 AI가 읽을 수 있을 만한 곳에 있는 .env 계열의 파일명을 찾습니다 (내용물은 표시하지 않습니다). 로그나 코드 내부의 키까지는 보지 못하므로, 위의 grep과 함께 사용해 주십시오.
npx github:supportapp3-lab/ai-secret-check scan
리포지토리: https://github.com/supportapp3-lab/ai-secret-check (Node.js 20 이상, MIT)
※ 2026-11-09에, Debian 13・Python 3.13.5・requests 2.34.2・urllib3 2.8.0, 더미 값・로컬 서버로 확인했습니다.
업데이트 정보나 간단한 Tips는 X (@supportapp3_lab)에서 발신하고 있습니다.