LLM이 보안에 취약한 WordPress 코드를 작성하는 이유와 이를 해결하기 위해 구축한 아키텍처
요약
LLM이 보안에 취약한 WordPress 코드를 생성하는 통계적 원인을 분석하고, 이를 해결하기 위해 확률적 생성기를 결정론적 시스템 내에 가두는 다단계 에이전트 아키텍처를 제안합니다.
핵심 포인트
- LLM은 보안 제약 없이 통계적 확률에 따라 다음 토큰을 예측함
- WordPress의 훅 구조는 보안 검증 누락 시 공격 범위가 매우 넓음
- 확률적 모델을 규칙 기반의 결정론적 시스템으로 감싸는 설계가 필요함
- 출력을 계획, 구조화, 감사하는 다단계 에이전트 시스템 구축
챗봇과 PHP에 대한 적나라한 진실
만약 당신이 범용 LLM(Large Language Model)에게 "WordPress 플러그인을 작성해줘"라고 요청해 본 적이 있다면, 이 이야기가 어떻게 끝날지 이미 알고 있을 것입니다. 당신은 단 하나의 400줄짜리 파일을 받게 됩니다. 그것은 잘못된 우선순위로 훅(hook)을 등록합니다. 정제되지 않은 $_POST 값을 사용하여 $wpdb에 직접 기록합니다. 세 가지 주요 버전 전에 폐기(deprecated)된 함수를 호출합니다. 그리고 방금 생성된 AJAX 핸들러는 액션 이름(action name)을 아는 인증되지 않은 방문자라면 누구에게나 기꺼이 실행될 것입니다.
좌절스러운 부분은 모델이 "멍청해서"가 아닙니다. 모델은 자신이 구축된 목적 그대로를 수행하고 있기 때문입니다.
범용 모델은 통계적인 다음 토큰 예측기(next-token predictor)입니다. 이 모델은 훌륭한 코드, 평범한 코드, 그리고 누군가가 튜토리얼에 복사해서 붙여넣은, 솔직히 말해 경악스러운 수준의 보안이 취약한 2014년 시대의 StackOverflow 코드 조각들이 포함된 거대한 인터넷 텍스트 더미를 학습했습니다. 당신이 플러그인을 요청할 때, 모델은 WordPress Plugin Handbook을 바탕으로 추론하는 것이 아닙니다. 모델은 자신이 흡수한 모든 것(좋은 것이든 나쁜 것이든)에 따라 가중치가 부여된 _다음으로 그럴듯한 문자(next plausible character)_가 무엇인지 예측하고 있는 것입니다. 이 과정에서 보안은 제약 조건이 아닙니다. 그것은 단지 수천 개의 보안에 취약한 패턴들과 경쟁하는 또 하나의 패턴일 뿐입니다.
따라서 우리가 답하고자 했던 질문은 "어떻게 모델을 더 똑똑하게 만들 것인가?"가 아니었습니다. 그것은 소프트웨어 엔지니어링 질문이었습니다: 확률적인 텍스트 생성기를, 정해진 규칙 세트를 위반하는 코드를 내보내기를 거부하는 결정론적 시스템(deterministic system) 내부에 어떻게 감쌀 것인가?
이 포스트는 우리가 도달한 아키텍처에 대한 안내서입니다. 이는 개발자에게 도달하기 전에 스스로의 출력을 계획하고, 구조화하며, 감사(audit)하는 다단계 에이전트 시스템(agentic system)입니다. 마법이나 허황된 말은 없습니다. 그저 출력을 신뢰할 수 있게 만든 디자인 패턴(design patterns)뿐입니다.
취약점 벡터: 왜 "자동 완성된" 플러그인이 위험한가
CMS(Content Management System) 맥락에서 왜 가공되지 않은 LLM 출력이 보안상의 책임(liability)이 되는지 구체적으로 살펴보겠습니다. 왜냐하면 그 실패 모드(failure mode)가 일반적인 애플리케이션 코드와는 다르기 때문입니다.
WordPress는 훅(hook) 중심의 환경입니다. 의미 있는 거의 모든 작업은 액션(actions)과 필터(filters)에 콜백(callbacks)을 연결함으로써 이루어지며, 이러한 콜백들은 빈번하게 데이터베이스를 건드리거나, HTML을 렌더링하거나, 권한이 필요한 작업(privileged operations)을 수행합니다. 이러한 아키텍처는 AI가 생성한 코드에 두 가지 결과를 초래합니다.
- 하나의 검증 누락이 미치는 영향 범위(blast radius)가 엄청납니다.
permission_callback없이 등록된 REST 엔드포인트는 사소한 버그가 아니라, 열려 있는 문과 같습니다.wp_verify_nonce()를 건너뛰는 AJAX 핸들러는 크롤러를 기다리는 CSRF 취약점입니다. - 위험한 코드 라인이 무해해 보입니다. 모델은
$wpdb->query("UPDATE ... WHERE id = " . $_GET['id'])"와 같은 코드를 자신 있게 생성할 것입니다. 이 코드는 컴파일되고, 데모에서는 "작동"합니다. 하지만 이는 교과서적인 SQL 인젝션(SQL injection)입니다.
WordPress에서 가장 중요한 세 가지 검증 사항인 입력값 정화(input sanitization), 출력 이스케이프(output escaping), 그리고 권한 부여(authorization)는 다음 토큰 예측기(next-token predictor)가 가장 놓치기 쉬운 세 가지 요소이기도 합니다. 왜냐하면 학습 데이터 분포(training distribution) 내에서 이들이 빈번하게 결여되어 있기 때문입니다. 제대로 작동하는 튜토리얼 코드 중에도 이를 생략하는 경우가 많습니다.
따라서 실제 엔지니어링 과제는 다음과 같은 한 문장으로 구체화되었습니다.
어떻게 하면 시스템이
sanitize_text_field(),wp_verify_nonce(), 그리고current_user_can()을 선택적인 스타일 제안이 아니라, 타협 불가능한 컴파일 요구 사항(non-negotiable compilation requirements)으로 취급하도록 강제할 수 있을까?
단순히 더 나은 프롬프트(prompt)만으로는 이 문제를 해결할 수 없습니다. 프롬프트는 설득이며, 확률적인 유도(probabilistic nudges)일 뿐입니다. 우리에게는 구조가 필요했습니다.
아키텍처: 도메인 특화 그라운딩 엔진 (A Domain-Specific Grounding Engine)
핵심 아이디어는 코드 생성을 단일 텍스트 완성(text-completion) 호출로 취급하는 것을 멈추고, 각각 하나의 작업만을 수행하며 서로 구조화된 데이터를 전달하는 전문화된 에이전트(agents)들의 파이프라인으로 취급하는 것입니다. 우리는 3단계 에이전트 사이클(three-tier agentic cycle)을 채택했습니다.
1단계 — 의도 분석 및 청사진 작성 ("PM" 에이전트)
첫 번째 에이전트는 PHP 코드를 단 한 줄도 작성하지 않습니다. 이 에이전트의 유일한 임무는 사용자의 평이한 영어 요청을 구조화된 JSON 매니페스트 (manifest)로 변환하는 것입니다. 예를 들어, _"편집자가 검토를 위해 게시물을 플래그할 수 있게 해주는 플러그인"_과 같은 프롬프트는 다음과 유사한 형태로 변환됩니다:
{
"plugin_slug": "post-review-flags",
"capabilities_required": ["edit_others_posts"],
...
이 매니페스트는 계약 (contract) 역할을 합니다. 코드가 존재하기 전, 시스템이 자신의 권한 (capabilities), 훅 (hooks), 그리고 논스 (nonce) 요구 사항을 사전에 _선언 (declare)_하도록 강제함으로써, 보안을 생성 과정에서의 우연한 사고가 아닌 계획 단계의 결정 사항으로 만듭니다. 만약 AJAX 핸들러가 여기서 "nonce": true로 선언된다면, 하위 에이전트들은 이를 준수할 의무를 갖게 됩니다.
2단계 — 문맥 인식 생성 ("Developer" 에이전트)
두 번째 에이전트는 매니페스트를 바탕으로 한 번에 하나의 모듈씩 실제 파일로 컴파일합니다. 여기서 중요한 아키텍처 설계 선택은 **파일 트리에 의해 강제되는 관심사의 분리 (separation of concerns)**입니다. 데이터 계층 로직 (data-layer logic), 컨트롤러 (controllers), 그리고 뷰 템플릿 (view templates)은 하나의 거대한 단일체 (monolith)가 아닌 별개의 파일로 생성됩니다.
하지만 에이전트가 각 파일을 고립된 상태에서 생성하는 것은 아닙니다. 에이전트는 다중 파일 의존성 그래프 (multi-file dependency graph)에 대한 활성 참조 맵 (reference map)을 유지하므로 (이에 대한 자세한 작동 방식은 아래 문맥 창 (context-window) 섹션에서 다룹니다), 컨트롤러에서 선언된 함수를 호출하는 뷰가 해당 함수를 인지할 수 있습니다.
3단계 — 결정론적 감사 루프 ("Security" 에이전트)
이 단계가 실제로 신뢰를 확보하는 부분입니다. 생성 후, 전담 에이전트가 생성된 코드를 파싱합니다. 이때 "모델에게 안전해 보이는지 묻는" 방식이 아니라, 구조를 따라가며 불변성 (invariants)을 검증하는 결정론적 실행 경로 (deterministic execution path)를 통해 검증합니다:
- 모든 등록된 AJAX/REST 콜백 (callback)은 그에 상응하는 권한 확인 (capability check)과 (상태를 변경하는 경우) 논스 검증 (nonce verification)을 포함합니다.
- 슈퍼 글로벌 (superglobal)에서 유래한 모든 값은 사용되기 전에 반드시 새니타이제이션 함수 (sanitization function)를 거칩니다.
- 출력에 도달하는 모든 동적 값은 echo되는 시점에 이스케이프 (escape) 처리됩니다 (지연 이스케이프 (late escaping)).
불변성 (invariant)이 실패하면, 코드는 거부되고 특정 위반 사항이 표시된 채 재생성을 위해 다시 라우팅됩니다. 루프가 닫힙니다. 감사를 통과하지 않은 것은 아무것도 배포되지 않습니다.
WPCoder AI 플러그인 생성기의 핵심 엔진을 설계할 때, 우리는 표준적인 의미론적 임베딩 (semantic embeddings)만으로는 충분하지 않다는 것을 깨달았습니다. 환각 (hallucinations)을 제거하기 위해, 우리는 생성 루프를 맞춤 제작된 WordPress 코어 파싱 레이어 (parsing layer) 내에 고정해야 했습니다. 이를 통해 모델은 최종 PHP 문자열을 작성하기 전에 활성 코어 API에 대해 훅 (hooks)을 동적으로 검증합니다. 따라서 환각된 함수 이름은 조용히 배포되는 대신 빠르게 실패하게 됩니다.
코드 분석: 챗봇 출력 vs. 근거 기반 에이전트 (Grounded Agent) 출력
아키텍처에 대한 주장은 쉽습니다. 차이점을 구체적으로 보여드리겠습니다.
예시 A — 전형적인 일반 AI 출력. 언뜻 보기에는 괜찮아 보입니다. 하지만 CSRF 취약점과 SQL 인젝션 (SQL injection) 위험을 동시에 안고 있습니다.
add_action('wp_ajax_save_setting', 'save_setting');
function save_setting() {
...
논스 (nonce)가 없습니다. 권한 확인 (capability check)도 없습니다. 가공되지 않은 $_POST 데이터가 SQL에 직접 삽입되었습니다. 이스케이프 처리되지 않은 출력도 있습니다. 그리고 wp_ajax_save_setting은 단지 훅 이름의 운으로 로그인한 사용자만 커버할 뿐입니다. 개발자는 권한 부여 (authorization)에 대해 전혀 고려하지 않았습니다.
예시 B — 근거 기반 에이전트 (grounded agent) 출력. 동일한 기능이지만, 모든 불변성 (invariant)을 충족합니다.
add_action('wp_ajax_save_setting', [$this, 'save_setting']);
public function save_setting(): void {
...
논스 (nonce)가 검증되었고, 권한이 확인되었으며, 입력값은 언슬래시 (unslashed) 및 정화 (sanitized)되었고, 출력은 이스케이프 (escaped)되었습니다. 또한 안전하지 않은 원시 $wpdb 쓰기 작업은 Options API로 대체되었습니다. 이 중 어느 것도 모델이 주의를 기울이기로 "기억"한 결과가 아닙니다. 이는 1단계의 매니페스트 (manifest)가 3단계의 감사 (audit)에 의해 강제된 결과입니다.
멀티 파일 플러그인을 위한 컨텍스트 윈도우 (Context Window) 장벽 해결하기
실제 멀티 파일 프로젝트를 생성하려는 모든 이들이 겪는 문제는 다음과 같습니다: 모델이 잊어버린다는 것입니다.
당신은 core.php를 생성하며, wp_enqueue_script('my-admin-bundle', ...)를 통해 스크립트 핸들(script handle)을 등록합니다. 세 개의 파일이 지나간 후 관리자 대시보드(admin dashboard)를 생성할 때, 모델은 컨텍스트(context) 내에서 해당 텍스트를 지나쳐 버렸기 때문에 다른 핸들을 만들어내거나, 등록된 적 없는 에셋(asset)을 인큐(enqueue)합니다. 대시보드는 아무것도 로드하지 못한 채 조용히 실행됩니다. 전형적인 컨텍스트 윈도우(context-window) 건망증입니다.
우리의 해결책은 단일 모델 호출 외부에서 존재하는 **공유 내부 상태 레지스트리 (shared internal state registry)**입니다. 각 하위 에이전트(sub-agent)가 파일을 생성할 때마다, 등록된 스크립트 핸들, 선언된 클래스 이름(class names), 공개 메서드 시그니처(public method signatures), 메타 키(meta keys), 훅 이름(hook names)과 같은 구조적 사실들을 이 레지스트리에 기록합니다. 다른 하위 에이전트가 해당 사실들에 의존하는 파일을 생성하기 전에, 관련 레지스트리 조각(slice)이 해당 에이전트의 컨텍스트에 주입됩니다.
따라서 대시보드 에이전트는 천 개의 토큰(token) 전의 핸들을 기억해내는 것이 아닙니다. 코어 에이전트가 생성하는 즉시 레지스트리에 기록했기 때문에, 'my-admin-bundle'이라는 정확한 문자열을 확정된 사실로서 전달받는 것입니다. 의존성 그래프(dependency graph)는 각 에이전트가 작업 메모리(working memory)에 유지해야 하는 것이 아니라 공유 상태(shared state)가 됩니다. 이 단 한 번의 변화가 고작 몇 개의 파일을 넘어선 플러그인들을 실제로 일관성 있게 만들었습니다.
코드 작성자에서 코드 감사자로의 전환
이 아키텍처가 실제로 변화시키는 것은 개발자의 일상입니다.
보일러플레이트(boilerplate)가 신뢰할 수 있게 되면, add_action을 타이핑하거나, wp_enqueue_script를 연결하거나, 똑같은 논스(nonce) 작업을 백 번째 반복하는 데 인지적 에너지를 소비하는 일을 멈추게 됩니다. 그 작업이 사라지는 것은 아닙니다. 대신 정확하게 생성되어 검토를 위해 당신에게 전달됩니다.
따라서 역할이 바뀝니다. 당신의 주요 역할은 _수동 타이핑_에서 _아키텍처 감사 (architectural auditing)_로 이동합니다. 당신은 생성된 매니페스트(manifest)를 읽으며 오직 인간만이 가질 수 있는 질문을 던지게 됩니다: '이 작업에 적절한 권한(capability)인가?', 'post_meta가 실제로 여기서 적절한 저장 계층인가, 아니면 커스텀 테이블을 사용해야 하는가?', '이 훅 우선순위(hook priority)가 다른 플러그인과 충돌하지 않는가?'와 같은 질문들 말입니다.
분명히 말씀드리자면, 이것은 결코 개발자를 대체하는 것이 아닙니다. 자신의 출력을 스스로 감사(audit)하는 시스템이라 할지라도, 모델이 볼 수 없는 요구사항에 비추어 문맥(context) 속에서 인간이 판단해야 하는 결과물을 여전히 생성하기 때문입니다. 에이전트 기반 생성(agentic generation)이 제공하는 것은 레버리지(leverage)입니다. 즉, 지루하고 오류가 발생하기 쉬운 기계적인 계층을 제거함으로써, 여러분의 주의력이 실제로 가치 있는 곳인 검증(verification), 통합(integration), 그리고 설계(design)에 집중될 수 있도록 해줍니다.
결론 및 커뮤니티 토론
실제로 신뢰할 수 있는 코드 생성 도구를 구축한다는 것은 단일 프롬프트 채팅 스크립트 방식을 포기하는 것을 의미합니다. 가공되지 않은 다음 토큰 예측기(next-token predictor)는 보안 측면에서 언제나 동전 던지기와 같습니다. 신뢰성은 그 주변을 감싸는 시스템에서 나옵니다. 즉, 사전에 구조화된 청사진(blueprinting) 설계, 공유 상태(shared state)를 가진 모듈형 생성, 그리고 불변성(invariants)을 위반하는 코드의 배포를 거부하는 결정론적 감사 루프(deterministic audit loop)가 필요합니다.
이러한 요소들 중 그 어느 것도 그 자체로 생소한 것은 아닙니다. 이는 평범한 소프트웨어 엔지니어링 패턴들입니다. 핵심은 확률적 생성기(probabilistic generator)를 엄격하고 도메인 특화된 규칙(domain-specific rules) 내로 제한하기 위해 이 패턴들을 적용하는 것입니다.
이제 커뮤니티에 질문을 던지겠습니다. 여러분은 개발 스택에 AI 도구를 통합할 때 보안 검증(security validation)을 어떻게 처리하고 계신가요? 프롬프트 수준의 가드레일(guardrails), 생성 후 정적 분석(static analysis), 인간의 검토 단계(human review gates), 아니면 완전히 다른 방식인가요? 댓글을 통해 의견을 나누어 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기