- Node.js의
axios를 사용하여 401과 같은 에러를 catch (err) 블록에서 처리할 때 (2026-11-10, Node.js 24.22.1・axios 1.21.1, 더미 값 및 로컬 서버로 실제 기기 확인). 한 번의 에러 출력만으로 약 1,000줄이 나왔으며, 키가 총 7군데에서 노출되었습니다 (console.log(err)를 사용했을 때, depth: 4로 표시한 경우).
console.log(err)의 기본 깊이에서도 키는 3군데에서 노출되었습니다.
JSON.stringify(err)를 사용할 때도 노출됩니다. 이는 에러 객체에 config.headers가 포함되어 있기 때문입니다.
- 만약
err.message만 사용한다면, 출력은 Request failed with status code 401로 표시되며 키는 노출되지 않았습니다. - axios 1.21.1에는 redact라는 설정이 있으며, 여기에 redact: ['authorization']를 추가하면 JSON.stringify(err)에서는 [REDACTED ****]로 대체되었습니다. 하지만 이 방법은 console.log(err)에는 적용되지 않아 키가 그대로 노출되었습니다 - 비교하자면, Node 표준의 fetch 연결 에러를 console.log(err)로 출력했을 때는 키가 포함되어 있지 않았습니다 (연결 거부 케이스 1건만 확인).
이 글은 'AI 에이전트와 비밀 정보' 검증 시리즈 중 하나입니다. 새로운 검증 내용은 X (@supportapp3_lab)에서도 공지하고 있습니다.
catch 블록에서 발생한 에러를 통째로 로그에 출력하는 부분을 찾아봅니다.
grep -rnE --include='*.js' --include='*.ts' 'console\.(log|error)\((err|error|e)\)|JSON\.stringify\((err|error|e)\)' . --exclude-dir=node_modules
찾아낸 부분이 axios 에러인지 여부는 전후 코드를 통해 확인해야 합니다.
API 호출이 실패했을 때, '일단 내용을 전부 보고 싶다'는 생각으로 console.log(err)나 console.error(error)를 작성하는 것은 흔한 방식입니다. AI에게 코드 생성을 요청할 때도 이러한 형태가 자주 나타납니다 (추측이며, 건수는 세지 않았습니다).
axios의 에러 객체에는 전송했던 리퀘스트 설정(config)이나, 전송된 헤더 문자열이 포함되어 있습니다. 따라서 에러 전체를 출력하면 키가 함께 노출됩니다.
터미널 출력은 로그 파일, CI(지속적 통합) 로그, 에러 모니터링 서비스, AI 에이전트에게 전달되는 에러 전문 등에 그대로 섞일 수 있습니다. Codex나 Claude Code처럼 명령을 실행하고 출력을 읽는 도구에서는 그 출력 또한 대화 내용에 포함됩니다.
항상 401을 반환하는 작은 HTTP 서버를 로컬에 띄우고 더미 키를 전송했습니다 (외부에는 전송하지 않았습니다).
// server.js
const http = require('http');
http.createServer((q, s) => {
...
// repro.js (발췌)
const axios = require('axios');
const util = require('util');
...
| 출력 방식 | 키 노출 여부 |
|---|
console.log(err) (기본 깊이) | 노출됨 (3군데) |
util.inspect(err, { depth: 4 }) | 노출됨 (7군데, 출력 1,026줄) |
JSON.stringify(err) | 노출됨 |
err.message | 노출되지 않음 |
redact: ['authorization']를 추가하여 JSON.stringify(err) 사용 시 | 노출되지 않음 ([REDACTED ****]) |
redact: ['authorization']를 추가하여 console.log(err) 사용 시 | 노출됨 |
Node 표준 fetch 연결 에러를 console.log(err)로 출력 시 | 노출되지 않음 |
console.log(err)의 출력 중 키가 포함된 줄입니다.
Authorization: 'Bearer sk-dummy-1234567890abcdef',
'Authorization: Bearer sk-dummy-1234567890abcdef\r\n' +
'Authorization: Bearer sk-dummy-1234567890abcdef\r\n' +
키를 params: { api_key: KEY }로 URL 쿼리에 넣은 경우에도, console.log(err) 출력에 params: { api_key: ... } 또는 GET /v1/me?api_key=... HTTP/1.1 형태로 나타났습니다 (err.message에는 나타나지 않았습니다).
axios 1.21.1의 lib/core/AxiosError.js를 보면, redact 기능을 사용하는 부분이 toJSON() 내부뿐이었습니다. README에도 "AxiosError#toJSON()가 호출될 때 일치하는 설정 키를 가립니다"라고 적혀 있습니다. console.log(err)는 toJSON()을 호출하지 않고 객체 내용을 그대로 표시하기 때문에, 가릴 수 없을 것으로 생각됩니다 (추측이며, 실제 측정 결과는 위의 표와 같습니다).
- 에러 전체를 출력하지 않고 필요한 항목만 출력합니다.
catch (err) {
console.error({
message: err.message,
...
}
제 환경에서는 이 출력에 키가 포함되지 않았습니다. 하지만, 키를 ?api_key=...와 같이 URL 문자열에 직접 작성한 경우에는, url 항목에 키가 그대로 나타났습니다 (실기 확인). 그럴 경우 url을 제외하거나 쿼리를 지운 후 출력합니다.
- JSON으로 전송하는 로그(에러 모니터링 등)에는
redact: ['authorization']도 추가합니다. 다만 console.log(err)에는 효과가 없으므로, 1번과 조합하여 사용합니다. - 키는 URL 쿼리가 아니라 헤더로 전달합니다 (API가 지원하는 경우).
- 에러 출력을 AI에 붙여넣거나 로그로 저장하기 전에,
Authorization이나 api_key가 없는지 확인합니다. - AI에게 코드를 요청할 때는 "에러 객체 전체를 로그로 출력하지 말라"는 조건을 추가합니다.
ai-secret-check scan
npx github:supportapp3-lab/ai-secret-check scan
리포지토리: https://github.com/supportapp3-lab/ai-secret-check (Node.js 20 이상, MIT)
※ 2026-11-10에, Debian 13, Node.js 24.22.1, axios 1.21.1, 더미 값 및 로컬 서버 환경에서 확인했습니다.
업데이트 정보나 간단한 Tips는 X(@supportapp3_lab)를 통해 발신하고 있습니다.