AI 코딩 도구 허용 목록 파일의 비밀값 노출은, 승인한 명령을 그대로 저장하는 도구(이번 조사 대상)에서 명령 실행을 '다시 묻지 않기'로 승인할 때 그 명령이 프로젝트 안 로컬 설정 파일(예: settings.local.json)에 허용 규칙으로 저장되고, 명령에 적어 둔 비밀번호·토큰·키까지 평문으로 남은 이 파일이 공개 저장소에 함께 커밋되어 누구나 읽을 수 있게 되는 노출이다.
보안 업체 Apiiro의 조사(2026년 11월 5일 공개, 11월 7일 보도)에서 깃허브 공개 저장소 103,617곳의 이 파일 사본 104,185개를 살펴보니 1,122개(1.1%)에 실제 비밀값이 들어 있었다. 서로 다른 비밀값 1,379개 가운데 데이터베이스 비밀번호가 379개, SSH 로그인 43개, 깃허브·깃랩·npm 토큰 42개, AWS 키 23개였다. 연구진은 키 형식에 맞거나 공개 호스트 옆에 적힌 값만 '실제 비밀값'으로 셌고 유효한지는 시험하지 않았으니, 전부가 지금도 쓸 수 있는 값이라는 뜻은 아니다. 그래도 이 노출은 공격 기법이 아니라, 비밀값을 명령줄에 직접 적는 습관과 내 PC에만 있어야 할 파일이 저장소에 같이 커밋되는 상황이 겹쳐서 생긴다. 도구 제작사는 지금까지 비밀값을 걸러내는 처리를 하지 않았고 막을 방법을 검토 중이라고 밝혔으니, 당분간은 쓰는 쪽에서 막아야 한다.
이 글은 자주 발생하는 실수 패턴, 점검·조치 절차, 자가진단을 정리했다.
한눈에 보는 핵심
다시 묻지 않기를 누르면 무엇이 어디에 저장되나요?
이번 조사 대상 도구의 공식 문서는 명령 실행을 '다시 묻지 않기'로 승인하면 그 승인이 프로젝트 안 설정 폴더의 settings.local.json 에 허용 규칙으로 저장된다고 설명합니다. 연구 보고서에 따르면 규칙이 명령 전체 문자열이면 그 안에 적은 비밀번호나 토큰도 평문으로 같이 남습니다. 도구가 npm run 같은 앞부분 패턴을 규칙으로 제안하는 경우도 있어서 모든 승인이 위험한 것은 아니지만, 비밀값이 든 명령을 승인하면 그 값이 규칙에 들어갈 수 있습니다.
내 PC에만 있는 파일이 어떻게 공개 저장소에 올라가나요?
도구는 이 파일을 처음 쓸 때 내 PC의 전역 git 제외 목록에 넣어 커밋에서 빠지게 합니다(공식 문서). 문제는 그 설정이 내 PC 한 곳에만 있다는 점입니다. 연구진은 나중에 저장소가 된 폴더, 다른 PC에서 받은 clone, 이미 커밋된 파일은 이 장치가 덮지 못한다고 짚었습니다. 비밀값이 나온 저장소 1,105곳을 추적한 결과 539곳에서는 AI 도구가 공동 저자로 기록된 커밋이 이 파일을 바꿨습니다.
파일을 지우고 다시 커밋하면 끝인가요?
아닙니다. 파일을 지워도 과거 커밋과 포크, 다른 사람의 clone에 값이 그대로 남기 때문입니다. GitHub 문서도 비밀값이면 첫 단계는 폐기와 재발급이라고 하고, 폐기하면 그 값으로 접근할 수 없으니 그것만으로 충분할 수도 있다고 설명합니다. 기록 정리는 그다음입니다.
이미 비밀 탐지 도구를 쓰고 있는데 안 잡히나요?
놓칠 수 있습니다. 연구에서 비밀값이 든 1,122개 파일 가운데 gitleaks는 564개, TruffleHog는 643개를 잡았고 215개는 둘 다 잡지 못했습니다. 키 형식을 찾는 도구는 PGPASSWORD= 뒤에 적힌 평범한 단어를 비밀번호로 보지 않기 때문입니다. 쓰는 도구에 가짜 값 한 줄을 넣어 시험하고, 못 잡으면 사용자 정의 규칙을 추가해야 합니다.
자주 발생하는 실수 패턴
| # | 패턴 | 위험도 |
|---|---|---|
| 1 | 스테이징 서버 로그인 조사나 마이그레이션을 시킬 때 도구가 제안한 명령에 비밀번호·토큰이 그대로 들어가는데(PGPASSWORD=값, mysql -p값, sshpass -p, Bearer 헤더), 그 명령을 '다시 묻지 않기'로 승인 | 치명 |
| 2 | 내 PC의 전역 제외 설정에만 기대고 저장소 .gitignore 에는 규칙이 없음(나중에 저장소가 된 폴더, 다른 PC에서 받은 clone, 이미 커밋된 파일은 막히지 않는다) | 높음 |
| 3 | 파일만 지우고 키는 그대로 둠(파일을 지워도 이전 버전에 든 비밀값 734개가 509개 저장소 기록에 남아 있었다) | 치명 |
| 4 | 커밋을 AI 도구에 맡기고 커밋 대상 파일 목록을 확인하지 않음(비밀값이 나온 저장소 1,105곳을 추적하니 539곳은 AI 공동 저자 커밋이 이 파일을 바꿨다) | 높음 |
| 5 | 편집기 확장의 승인 창에서 저장 위치로 팀 공유 설정 파일을 고름(저장소에 올리는 파일이라 규칙이 팀 전체에 배포되고, 조사한 공유 설정 76,011개 중 298개에 비밀값이 있었다) | 높음 |
| 6 | 키 형식만 보는 비밀 탐지 도구를 믿음(비밀값이 든 1,122개 중 215개는 gitleaks와 TruffleHog 둘 다 놓쳤다) | 높음 |
재발 방지 설정: .gitignore, 커밋 전 검사, 기록 정리
# 1) 저장소 자체 .gitignore 에 넣는다. 도구가 넣어 주는 전역 무시는 내 PC 한 곳에만 있다
cat >> .gitignore <<'EOF'
# AI 코딩 도구의 개인용 허용 목록(승인한 명령이 그대로 저장됨)
**/settings.local.json
EOF
# 2) 이미 추적 중이면 추적만 끊는다(파일은 디스크에 남는다). 경로는 자가진단 1단계 결과
git rm --cached -- '<추적 중인 경로>'
git add -- .gitignore
git status --short # 삭제(D)와 .gitignore 변경(M)만 있는지 본다
git commit -m "chore: 로컬 허용 목록 파일 추적 해제"
# 3) 명령줄 비밀번호를 잡는 사용자 정의 규칙. 기본 규칙은 키 형식 위주라 놓친다
cat > .gitleaks.toml <<'EOF'
[extend]
useDefault = true
[[rules]]
id = "cmdline-password"
description = "명령줄에 직접 적은 비밀번호"
regex = '''(?:PGPASSWORD=|sshpass\s+-p\s*|mysql(?:\s+[^\n|]*?)?\s-p)[\\"']{0,2}([^\s$"'|\\]{4,})'''
secretGroup = 1
keywords = ["pgpassword", "sshpass", "mysql"]
EOF
# 4) 커밋 전 검사 hook (기존 hook 이 있으면 덮어쓰니 내용을 합친다). gitleaks 가 있으면 함께 돌린다
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
if git diff --cached --name-only --diff-filter=ACMR | grep -qE '(^|/)settings\.local\.json$'; then
echo "차단: 로컬 허용 목록 파일이 커밋 대상에 있다 (git restore --staged 로 뺀다)"
exit 1
fi
if command -v gitleaks >/dev/null 2>&1; then
gitleaks git --pre-commit --redact --staged --verbose
fi
EOF
chmod +x .git/hooks/pre-commit
# 5) 이미 올라갔다면: 키를 먼저 폐기한 뒤에만. git-filter-repo 2.47 이상
# 마지막 push 는 원격의 브랜치·태그를 이 clone 기준으로 덮어쓰고, 여기 없는 원격 ref 는 지운다.
# 팀에 알려 푸시를 멈춘 뒤 괄호까지 통째로 붙여 넣는다. 중간에 하나라도 실패하면 거기서 멈춘다.
(
set -e
git clone --mirror <저장소 주소> ../backup-before-cleanup.git # 되돌릴 때 쓰는 백업
git clone <저장소 주소> ../cleanup
cd ../cleanup
git-filter-repo --sensitive-data-removal --invert-paths --path-glob '*settings.local.json'
git remote get-url origin >/dev/null 2>&1 || git remote add origin <저장소 주소>
git remote -v
git push --force --mirror --dry-run origin # 바뀌거나 지워질 ref 목록을 먼저 본다
read -r -p '원격 주소와 ref 목록이 맞으면 yes 입력: ' ok
[ "$ok" = yes ]
git push --force --mirror origin
)
이 파일은 설정이 아니라 승인 기록이라서, 비밀번호가 든 명령을 승인해 규칙이 명령 전체로 저장되면 그 값이 가공 없이 평문으로 남는다. 그래서 막는 순서도 파일이 올라가지 못하게 하고, 비밀값을 명령에 쓰지 않게 하고, 마지막에 탐지하는 순이다.
코드는 bash 기준(Windows는 Git Bash)이고, hook은 저장소에 같이 올라가지 않으니 팀에서는 core.hooksPath 로 공유 폴더를 지정하며, 3번 규칙은 시작점이라 가짜 값 한 줄로 먼저 시험한다(원 조사: Apiiro 보고서).
자가진단
# 저장소 맨 위 폴더에서 실행한다. Git Bash, macOS, Linux 셸 기준이다
# 3)과 4)는 출력에 비밀값이 그대로 보일 수 있으니 화면 공유 중에는 돌리지 않는다
# 1) 지금 추적 중인가 (하위 폴더에 흩어진 사본까지 잡는다)
git ls-files '*settings.local.json'
# 판정: 한 줄이라도 나오면 이미 저장소에 올라가 있다. 비어 있어도 2단계는 한다
# 커밋한 적 없이 디스크에만 있는 사본은 find . -name settings.local.json 으로 찾아 열어 본다
# 2) 지금은 없어도 과거에 올라간 적이 있는가 (모든 브랜치와 태그 기준)
git log --all --oneline -- '*settings.local.json'
# 판정: 커밋이 하나라도 나오면 기록에 남아 있다. 파일을 지웠어도 마찬가지다
# 3) 지금 추적 중인 JSON 에 허용 규칙이 들어 있는가 (팀 공유 설정 파일 포함)
git grep -nE '"(Bash|PowerShell)\(' -- '*.json'
# 판정: 규칙을 눈으로 읽는다. 비밀번호, 토큰, 접속 문자열이 보이면 폐기 대상이다. npm run 처럼 앞부분 패턴뿐이면 정상이다
# 4) 과거에 커밋된 규칙 전체 (이름에 settings 가 든 JSON 만 본다)
git log --all -p --format= -- '*settings*.json' | grep -E '^[+-][[:space:]]*"(Bash|PowerShell)\(' | sort -u
# 판정: 나온 값 전부를 폐기 목록에 적는다. 지금 파일에 없는 값도 포함한다
# 5) 이 저장소가 스스로 막고 있는가
grep -n 'settings.local.json' .gitignore
# 판정: 아무것도 안 나오면 내 PC의 전역 제외 설정에만 기대는 상태다. 다른 PC에서 받은 clone 에서는 막히지 않는다
# 6) 조직 전체 (GitHub CLI 로그인 필요. 기본 브랜치만 검색되고, 로그인한 계정이 볼 수 있는 저장소만 나온다)
gh search code allow --owner <조직> --filename settings.local.json --limit 100
# 판정: 나온 저장소마다 1~4단계를 반복한다. 비어 있어도 과거 기록까지 없다는 뜻은 아니다
위 시그널이 발견되면 결함이다. GitHub 문서: 저장소에서 민감한 데이터 제거(첫 단계는 키 폐기와 재발급)를 함께 참조하라.
조치 절차
| 순위 | 조치 |
|---|---|
| 1 | 파일과 기록에서 찾은 비밀값을 전부 폐기하고 재발급한다. 현재 파일뿐 아니라 과거 버전과 팀 공유 설정에 있던 값까지 포함하고, 노출된 기간의 접속 기록에서 낯선 접근이 없었는지 본다. 기록 정리보다 반드시 먼저 한다 |
| 2 | 저장소 .gitignore 에 settings.local.json 규칙을 넣고 git rm --cached 로 추적을 끊는다. 커밋한 적이 없어도 내 PC의 파일을 열어 비밀값이 든 규칙부터 지운다 |
| 3 | 키 폐기가 끝난 뒤 기록에서 파일을 제거한다(백업, 팀 푸시 중지, 새 clone, git-filter-repo, 바뀔 ref 확인, 강제 푸시). 강제 푸시는 원격의 다른 사람 작업까지 덮어쓸 수 있으니 백업과 푸시 중지를 먼저 한다. 비밀값이 처음 들어간 커밋부터 이후 커밋의 해시가 모두 바뀌고 포크와 다른 사람의 clone에는 값이 남으니 팀에 알린다. 캐시된 화면과 풀 리퀘스트 참조는 GitHub 지원에 요청하지만, 폐기로 위험이 줄어든 경우에는 받아주지 않을 수 있다 |
| 4 | 명령에 비밀값을 쓰지 않는다. PostgreSQL 은 비밀번호 파일(.pgpass, Windows는 pgpass.conf), MySQL 은 옵션 파일이나 mysql_config_editor, SSH 는 키 인증을 쓰고, API 토큰은 비밀 관리 도구가 실행 시점에 환경변수로 넣게 해서 명령에는 $변수 이름만 적는다. PGPASSWORD 도 공식 문서가 권장하지 않는다. AI 도구 대화창에도 비밀값을 붙여넣지 않는다 |
| 5 | '다시 묻지 않기'는 비밀값이 끼지 않는 명령의 앞부분 패턴(예: npm run 으로 시작하는 명령)에만 쓴다. 비밀값이 필요한 명령은 그때마다 한 번만 승인하거나 직접 실행하고, 편집기 확장에서 저장 위치를 고를 수 있어도 팀 공유 설정 파일은 고르지 않는다 |
| 6 | 탐지를 겹쳐 둔다. 커밋 전 hook, 저장소의 푸시 보호와 비밀 스캔 설정, 정기 기록 검사다. 도구가 만든 폴더(설정, 대화 기록, 메모 파일)도 .env 와 같이 스캔 대상에 넣고(같은 조사에서 대화 기록 파일 2,523개 중 120개에도 비밀값이 있었다), 쓰는 도구가 명령 안 비밀번호를 잡는지 가짜 값 한 줄로 시험한다 |
지금 해야 할 것
실무자: 저장소와 기록은 위 명령으로 직접 확인하고 키 폐기부터 하라. CodeScan 무료 스캔은 저장소를 보지 않고, 배포한 사이트에서 /.env 와 /.git 폴더가 웹에 열려 있는지만 확인한다(.git 이 열려 있으면 기록에 남은 파일도 내려받을 수 있다).
CISO·임원: 조직 저장소와 설정에 노출된 키 점검은 SENTRIX 보안 상담으로.