모델 출력은 사용자 입력이다 — SQL·HTML·셸로 흐를 때
한 줄 요약
모델의 출력이 SQL·브라우저·셸·DB 쓰기로 흘러가면, 옛 주입 공격이 모델을 거쳐 돌아옵니다. 모델 출력은 사용자 입력과 똑같이 믿을 수 없는 값으로 다루고, 받는 쪽의 문맥에 맞는 방어(의도 고르기·매개변수·이스케이프·셸 없는 실행·스키마 검증)를 겁니다.
왜 이게 필요했나
앞의 세 모듈에서 보았듯 모델의 출력은 공격자가 쓸 수 있습니다. 문서 속 지시, 사용자의 질문, 도구 결과 어디서든요. 그런데 많은 앱이 모델의 출력을 「우리 시스템이 만든 값」 으로 여겨 다음 단계에 그대로 넘깁니다. 자연어를 SQL 로 바꿔 실행하는 보고서 도우미, 모델의 답을 HTML 에 끼워 보여 주는 화면, 모델이 정한 파일 이름으로 셸 명령을 만드는 내보내기, 모델이 정리한 메모를 문자열로 이어 붙인 INSERT 가 그렇습니다. 그러면 SQL 주입·XSS·명령 주입이라는 오래된 문제가 「모델이 썼으니 괜찮다」 는 착각 아래 다시 열립니다. OWASP LLM Top 10 2025 는 이것을 부적절한 출력 처리(LLM05 Improper Output Handling)로 둡니다.
어떻게 동작하나
| 흘러가는 곳 | 하지 말 것 | 할 것 |
|---|---|---|
| SQL 읽기 | 모델이 쓴 SQL 을 실행 | 서버가 가진 질의 가운데 하나를 모델이 「이름으로」 고르게 하고, 값은 매개변수로, 연결은 읽기 전용으로 |
| HTML | 답을 그대로 끼워 넣기 | 넣을 자리의 문맥(본문·속성)에 맞게 이스케이프 — 질문을 되비추는 자리도 같다 |
| 셸 | 문자열을 이어 shell=True 로 실행 | 셸을 거치지 않는 함수(파일 복사)나 인자 목록, 이름은 허용 규칙(글자·숫자·-·_ 와 확장자)으로 |
| SQL 쓰기 | 값을 따옴표 안에 이어 붙이기 | 자리표시자, 그리고 그 전에 스키마 검증(형·길이·참조하는 값이 있는지) |
읽기 전용 연결은 따로 강조할 만합니다. SQL 문자열을 검사해 DROP·UPDATE 를 거르는 방식은 변형을 끝없이 따라가야 하지만, mode=ro 로 연 연결은 엔진이 쓰기를 거절합니다. 문자열 검사가 아니라 엔진이 지키는 경계라는 점에서 매개변수화와 같은 종류의 방어입니다.
스키마 검증은 「모델이 형식을 지켰는지」 를 믿지 않는 일입니다. 숫자여야 할 자리에 문자열이, 300자여야 할 자리에 1000자가, 있는 고객이어야 할 자리에 없는 이름이 올 수 있습니다. 맞지 않으면 실행하지 않고 4xx 로 거절합니다 — 500 으로 죽거나 고쳐서 받아 주지 않습니다.
한 가지를 덧붙이면, 이스케이프는 「어디에 넣느냐」 에 따라 다릅니다. HTML 본문, 속성 값, 자바스크립트 문자열, URL 은 각각 위험한 글자가 다르고, 그래서 OWASP 의 XSS 방지 지침은 넣는 자리의 문맥에 맞는 인코딩을 고르라고 합니다. 이 실습은 본문 문맥 하나만 다루지만, 모델 답을 링크 주소나 스크립트 안에 넣는 설계라면 같은 함수로는 부족합니다. 그런 자리는 애초에 모델 출력을 넣지 않는 쪽이 낫습니다.
셸도 같은 이야기입니다. 따옴표로 감싸는 함수(shlex.quote)는 명령 주입을 막지만 .. 로 디렉터리를 거슬러 오르는 이름까지 막지는 못합니다. 그래서 이름은 허용 규칙으로 좁히고, 셸이 필요 없는 일(파일 복사)은 셸 없이 합니다. 「위험한 글자를 고쳐서 받아 주기」 보다 「규칙에 맞지 않으면 거절하기」 가 단단한 까닭은, 고치는 쪽은 공격자가 고른 입력의 모든 경우를 알아야 하지만 거절하는 쪽은 허용한 모양만 알면 되기 때문입니다.
이 코스의 가짜 모델은 지시가 있으면 늘 공격자의 글을 그대로 출력합니다. 진짜 모델에서는 그런 출력이 가끔, 예측할 수 없이 나옵니다. 출력 처리 방어는 그 「가끔」 이 언제 오든 같은 결과를 내도록 받는 쪽에서 거는 것이고, 그래서 모델이 얼마나 착한지와 무관하게 시험할 수 있습니다.
현장에서 만나는 모습
자연어 질의 도구가 모델이 만든 질의로 권한 밖의 표, 곧 다른 고객의 데이터를 그대로 돌려준 일, 채팅 화면이 모델 답 속 태그를 그대로 그려 사용자의 브라우저에서 바깥 주소로 요청이 나간 일, 「보고서를 이 이름으로 저장해 줘」 기능이 이름 속 셸 문법을 실행한 일이 대표적입니다. 이 실습의 마지막 사건은 코스 전체를 묶습니다. 쿠폰 코드 셋이 바깥에서 쓰였는데, 하나는 모델이 쓴 SQL 로, 하나는 시스템 문장 추출로, 하나는 내보내기 셸 명령으로, 하나는 협력사에 보낸 기록 묶음으로 — 그중 셋이 실제 경로입니다.
받는 쪽 방어를 넣었다면 기록도 바꿉니다. 모델이 낸 SQL·HTML·명령을 그대로 적던 기록은 그 자체로 공격 문자열의 보관소가 되고, 다른 도구가 그 기록을 다시 읽어 실행하면 같은 문제가 한 번 더 납니다. 고친 뒤에는 모델이 고른 의도 이름, 거절한 까닭, 요청 id 처럼 판단에 필요한 것만 남깁니다.
다음 실습에서 할 것
보고서 도우미의 질의 기록에서 모델이 낸 위험한 SQL 을 세고, 질의를 의도 고르기·매개변수·읽기 전용으로, 화면을 이스케이프로, 내보내기를 셸 없는 복사와 이름 규칙으로, 메모 저장을 스키마 검증과 자리표시자로 고칩니다. 마지막으로 쿠폰 유출 사건의 세 경로를 가리고 막고, 새어 나간 쿠폰만 바꿉니다.