TT Lab
블로그 시작하기
배우기 러닝패스 코스

LLM 앱 보안 — 주입에서 출력 처리까지

비밀과 개인정보가 새는 자리 — 넣지 않고, 거르고, 가리고, 감지한다

TT Lab 에서 이어서 보기

목표

고객 지원 봇에서 시스템 문장의 키, 원문 기록, 고객 구분 없는 검색이라는 세 출구를 찾아 막고, 카나리로 프롬프트 유출을 감지합니다.

왜 중요한가

모델 입력에 들어간 것은 언젠가 답으로 나올 수 있고, 입력을 적는 기록은 그 내용을 더 많은 곳으로 복제합니다. 「모델에게 말하지 말라고 적어 두기」 는 경계가 아닙니다 — 진짜 모델은 확률적이라 대부분 지키다가 어느 날 지키지 않습니다. 이 실습은 비밀을 아예 넣지 않고(도구 층), 남의 것은 넣기 전에 거르고(검색), 기록은 가리고, 그래도 새면 카나리로 알아채는 순서로 고칩니다. 채점기는 채점할 때마다 새 키·카나리·전화번호를 만든 임시 데이터로 앱을 띄워, 그 값이 응답·기록 어디에 남는지 봅니다.

단계

  1. python3 /opt/fixtures/llmsec/kit.py support 로 재료를 펼친 뒤 /root/llmsec/support/app.py 를 127.0.0.1:8102 에 띄워 두세요(0.0.0.0 금지, 터미널을 닫아도 살아 있게). curl 127.0.0.1:8102/health 의 seed 가 펼칠 때 받은 씨앗과 같아야 합니다.
  2. 어제 기록 /root/llmsec/support/logs/app-2026-11-04.log 에 원문으로 남은 것을 세어 /root/llmsec/support/exposure.txt 에 tokens=<count>(기록에 나온 서로 다른 사용자 토큰 수), phones=<count>(서로 다른 전화번호 수), key_lines=<count>(협력사 키 API 키: pk-… 가 든 줄 수) 세 줄을 적으세요.
  3. build_system·run_tool 을 고쳐 협력사 키를 시스템 문장에서 빼고, 주문 조회 도구가 실행될 때 서버가 config.json 에서 직접 읽게 하세요. 시스템 문장을 꺼내 달라는 요청에도 키가 응답·기록 어디에도 나오지 않고, 주문 조회는 그대로 돼야 합니다.
  4. log_request 를 고쳐 data/app.log 에 요청마다 한 줄(rid·사용자 이름)은 남기되 X-Token 값·전화번호·이메일·협력사 키가 원문으로 남지 않게 하세요. 전화번호는 끝 네 자리만 남깁니다(***-****-1234).
  5. build_system·check_output 을 고쳐 config.json 의 canary 값을 시스템 문장에 넣고, 응답에 그 값이 보이면 답을 통째로 막고 data/audit.log 에 그 요청의 rid 와 "event": "canary" 가 든 JSON 줄을 남기세요. 정상 답은 그대로 나가야 합니다.
  6. find_tickets 를 고쳐 모델에게 넣는 티켓을 요청한 고객의 것으로만 거르세요. 남의 티켓 낱말로 물어도 남의 정보가 답에 섞이지 않고, 자기 티켓으로는 답해야 합니다.
  7. incident ls-secret-leak 을 실행하면 사건이 납니다. 고객 세 명의 전화번호가 회사 밖에서 발견됐습니다(/srv/ls-support/found.txt). 고객마다 경로를 찾아 원천을 정리하고 코드를 실습 3~6단계처럼 고친 뒤 /root/incident/ls-secret-leak/report.txt 에 <customer>: <code> 세 줄과 evidence: <log line> 을 적으세요. 코드는 cross-tenant(다른 고객의 티켓이 답에 섞임), log-bundle(협력사에 보낸 진단 묶음), misfiled-ticket(주인이 잘못 적힌 티켓), prompt-extract(시스템 문장 추출) 가운데 하나입니다.

참고

앱을 펼치고 루프백에 띄운다

python3 /opt/fixtures/llmsec/kit.py support 로 재료를 펼친 뒤 /root/llmsec/support/app.py 를 127.0.0.1:8102 에 띄워 두세요(0.0.0.0 금지, 터미널을 닫아도 살아 있게). curl 127.0.0.1:8102/health 의 seed 가 펼칠 때 받은 씨앗과 같아야 합니다.

방향: 실습 앱은 바깥에 열 이유가 없습니다 — 듣는 주소가 곧 노출 범위입니다. 도구: ss -ltnp, curl, setsid·nohup. 구체: 펼치기 명령은 위 그대로입니다. 띄운 뒤 ss -ltn 에서 8102 번이 127.0.0.1 로 보이는지, /health 의 seed 가 /root/llmsec/support/data/seed.txt 와 같은지 보세요.

어제 기록에 원문으로 남은 것을 센다

어제 기록 /root/llmsec/support/logs/app-2026-11-04.log 에 원문으로 남은 것을 세어 /root/llmsec/support/exposure.txt 에 tokens=<count>(기록에 나온 서로 다른 사용자 토큰 수), phones=<count>(서로 다른 전화번호 수), key_lines=<count>(협력사 키 API 키: pk-… 가 든 줄 수) 세 줄을 적으세요.

방향: 원문 기록은 프롬프트에 들어간 모든 것의 복사본입니다. 무엇이 몇 개 복제됐는지 세는 것이 피해 범위를 정하는 첫걸음입니다. 도구: grep -o, sort -u, wc -l, jq, data/users.json. 구체: 토큰은 users.json 의 값이 기록에 나오는지로, 전화번호는 서로 다른 값의 수로, 키는 줄 수로 셉니다 — 셋이 세는 단위가 다릅니다.

키를 시스템 문장에서 도구 층으로

build_system·run_tool 을 고쳐 협력사 키를 시스템 문장에서 빼고, 주문 조회 도구가 실행될 때 서버가 config.json 에서 직접 읽게 하세요. 시스템 문장을 꺼내 달라는 요청에도 키가 응답·기록 어디에도 나오지 않고, 주문 조회는 그대로 돼야 합니다.

방향: 모델은 키가 필요하지 않습니다. 키가 필요한 것은 도구를 실행하는 서버 코드입니다. 도구: build_system, run_tool, jload("config.json"). 구체: 시스템 문장에서 키 줄을 없애고, run_tool 이 시스템 문장을 뒤지는 대신 설정을 읽게 바꾸세요. 채점기는 키를 새로 만들어 응답과 config.json 밖의 모든 파일을 봅니다.

기록은 남기되 가린다

log_request 를 고쳐 data/app.log 에 요청마다 한 줄(rid·사용자 이름)은 남기되 X-Token 값·전화번호·이메일·협력사 키가 원문으로 남지 않게 하세요. 전화번호는 끝 네 자리만 남깁니다(***-****-1234).

방향: 끄면 조사할 수 없고, 다 적으면 기록이 유출 경로가 됩니다. 무엇을 남길지 고르는 일입니다. 도구: re.sub, json.dumps. 구체: 머리글은 통째로 남기지 말고 신원은 이름으로, 질문·문맥·답의 번호와 이메일은 정규식으로 가리세요. 시스템 문장은 기록할 이유가 없습니다.

카나리로 프롬프트 유출을 감지한다

build_system·check_output 을 고쳐 config.json 의 canary 값을 시스템 문장에 넣고, 응답에 그 값이 보이면 답을 통째로 막고 data/audit.log 에 그 요청의 rid 와 "event": "canary" 가 든 JSON 줄을 남기세요. 정상 답은 그대로 나가야 합니다.

방향: 카나리는 비밀 대신 넣어 두는 표식입니다. 그것이 출력에 보이면 시스템 문장이 샌 것이니, 카나리만 지우지 말고 답을 통째로 막습니다. 도구: build_system, check_output, audit(). 구체: 카나리는 매번 설정에서 읽고, 비교 전에 공백·대소문자를 고르게 하세요. 진짜 모델이라면 바꿔 말해 빠져나갈 수 있다는 한계도 기억해 둡니다.

남의 티켓은 넣기 전에 거른다

find_tickets 를 고쳐 모델에게 넣는 티켓을 요청한 고객의 것으로만 거르세요. 남의 티켓 낱말로 물어도 남의 정보가 답에 섞이지 않고, 자기 티켓으로는 답해야 합니다.

방향: 입력에 들어간 것은 말할 수 있는 것입니다. 권한은 검색 단계에서 겁니다. 도구: find_tickets 의 user, tickets.json 의 owner. 구체: 점수를 매기기 전에 주인이 다른 티켓을 빼세요.

사건 — 세 고객의 번호가 바깥에서 발견됐다

incident ls-secret-leak 을 실행하면 사건이 납니다. 고객 세 명의 전화번호가 회사 밖에서 발견됐습니다 — 발견된 조각은 /srv/ls-support/found.txt, 봇은 /srv/ls-support/app.py, 데이터·기록은 /srv/ls-support/data/, 지난주 협력사에 보낸 진단 묶음은 /srv/ls-support/support-bundle.log 입니다. 고객마다 번호가 나간 경로를 찾아 원천을 정리하세요 — 잘못 적힌 티켓 주인은 번호의 주인으로 바로잡고, 시스템 문장의 번호는 빼고, 진단 묶음은 줄을 남긴 채 번호만 가립니다. 코드 /srv/ls-support/app.py 는 실습 3~6단계에서 고친 결함을 모두 막아야 합니다(키는 도구 층, 기록은 가려서, 카나리, 검색은 고객별로 — 채점기가 그 공격을 다시 보냅니다). /root/incident/ls-secret-leak/report.txt 에 <customer>: <code> 세 줄과 evidence: <log line> 을 적습니다. 코드는 고객마다 다음 중 하나입니다: cross-tenant(다른 고객의 티켓이 답에 섞임), log-bundle(협력사에 보낸 진단 묶음), misfiled-ticket(주인이 잘못 적힌 티켓), prompt-extract(시스템 문장 추출).

방향: 증상은 셋 다 「번호가 바깥에 있다」 입니다. 발견된 조각의 모양 그대로가 어느 기록에서 나올 수 있었는지가 경로입니다. 도구: grep -F, jq, tickets.json 의 owner, users.json 의 phone. 구체: 조각을 그대로 grep 해 app.log 의 답(answer)인지, 진단 묶음의 질문(q)인지 보세요. 답이라면 그 요청을 한 사람과 문맥에 들어간 티켓의 주인을, 번호의 실제 주인(users.json)과 견주면 다른 고객의 티켓이 섞인 것과 티켓 주인이 잘못 적힌 것이 갈립니다.