AI 가 만든 워드프레스 코드를 검토하다 보면 특정한 인상을 받게 됩니다. 모양은 관용적인데 안전장치만 빠져 있는 경우가 반복된다는 것입니다. 훅 이름도 맞고 파일 구조도 그럴듯한데, 정확히 네 자리에서 자주 비어 있습니다. 이 네 자리를 먼저 보는 습관을 들이면 검토가 짧아집니다.
이것은 도구를 탓하는 이야기가 아닙니다. 사람이 급하게 쓴 코드에서도 같은 자리가 빕니다 — 다만 AI 는 많은 양을 빠르게 만들어 내므로 같은 비율이라도 절대량이 늘어납니다. 그래서 검토를 절차로 만들어 두는 편이 낫습니다.
패턴 ① 이스케이프 없는 출력
가장 흔합니다. 변수를 화면에 내보내는 자리에서 이스케이프 함수가 빠집니다. 문맥에 맞는 함수를 골라야 한다는 점도 자주 놓칩니다 — 본문은 esc_html(), 속성값은 esc_attr(), 주소는 esc_url(), 서식이 필요한 본문은 wp_kses_post() 입니다.
// 위험 — 사용자 값이 그대로 나간다
echo '<h2 title="' . $label . '">' . $title . '</h2>';
// 안전 — 자리마다 맞는 함수를 쓴다
echo '<h2 title="' . esc_attr( $label ) . '">' . esc_html( $title ) . '</h2>';
검토 요령은 간단합니다. 변경된 파일에서 echo 와 <?= 를 전부 찾아 각 줄에 이스케이프가 있는지 봅니다. 없다면 그 값이 어디서 왔는지 따라가고, 출처가 요청 · DB · 외부 API 중 하나라면 반드시 붙입니다.
패턴 ② 문자열로 조립한 SQL
두 번째는 $wpdb 직접 쿼리입니다. 변수를 큰따옴표 문자열 안에 그대로 끼워 넣은 쿼리가 자주 나오는데, 이것이 SQL 인젝션의 교과서적 형태입니다.
// 위험 — 값이 쿼리 문자열에 이어 붙는다
$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->posts} WHERE post_author = $author" );
// 안전 — 값과 자리표시자를 분리한다
$rows = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_author = %d", $author )
);
주의할 점이 하나 더 있습니다. prepare() 를 부르기만 하면 안전해지는 것이 아니라, 값이 자리표시자로 들어가야 안전합니다. 값을 먼저 문자열에 끼워 넣은 뒤 그 결과를 prepare() 에 넘기는 코드는 이름만 안전합니다. 그리고 테이블명 · 컬럼명은 자리표시자로 넣을 수 없으므로, 그 자리는 허용 목록으로 고릅니다.
애초에 WP_Query 나 get_posts() 로 되는 일인지 먼저 묻는 것도 검토의 일부입니다. 직접 쿼리는 필요할 때만 씁니다.
패턴 ③ 권한 확인과 nonce — 둘 다 필요합니다
저장 · 삭제 · 상태 변경을 하는 처리에는 두 가지 확인이 모두 필요합니다. 자주 나오는 실수는 둘 중 하나만 있는 코드입니다.
nonce 는 “이 요청이 우리 화면에서 시작됐는가” 를 확인합니다. 다른 사이트가 로그인된 사용자의 브라우저를 이용해 요청을 보내는 것을 막습니다. 권한 확인은 “이 사용자가 이 일을 할 자격이 있는가” 를 확인합니다. 둘은 서로를 대신하지 못합니다 — nonce 만 있으면 권한 없는 로그인 사용자가 우리 화면을 통해 그 일을 할 수 있고, 권한 확인만 있으면 관리자를 속여 요청을 보내게 하는 경로가 남습니다.
// 저장 처리의 첫 두 줄
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( '권한이 없습니다.', 'acme' ), 403 );
}
check_admin_referer( 'acme_save_settings' );
REST 라우트라면 같은 일을 permission_callback 이 합니다. 이 인자를 __return_true 로 채운 코드가 생성물에서 종종 보이는데, 그것은 인증을 끈 것이지 통과시킨 것이 아닙니다.
패턴 ④ 경로 검증 없는 파일 접근
마지막은 파일을 읽거나 지우는 코드입니다. 요청 값이 경로의 일부가 되는 순간, 상위 디렉터리로 빠져나가는 입력을 막을 방법이 필요합니다.
// 위험 — 요청 값이 그대로 경로가 된다
readfile( $base . '/' . $_GET['f'] );
// 안전 — 파일명만 취하고, 정규화한 경로가 기준 밖이면 거부한다
$name = wp_basename( sanitize_file_name( wp_unslash( $_GET['f'] ?? '' ) ) );
$path = realpath( $base . '/' . $name );
if ( ! $path || ! str_starts_with( $path, realpath( $base ) ) ) {
wp_die( esc_html__( '잘못된 요청입니다.', 'acme' ), 400 );
}
핵심은 정규화한 뒤에 비교한다는 점입니다. 문자열 상태에서 위험한 패턴을 걸러 내려는 시도는 우회되기 쉽고, realpath() 로 실제 경로를 확정한 다음 기준 디렉터리로 시작하는지 보는 방식은 그 우회 대부분을 무의미하게 만듭니다. 업로드를 다룬다면 확장자 허용 목록과 wp_check_filetype_and_ext() 도 함께 봅니다.
검토를 도구로 옮깁니다
네 패턴은 사람이 매번 눈으로 볼 필요가 없습니다. 상당수는 정적 분석이 잡아 줍니다 — 워드프레스 코딩 표준 규칙집에는 이스케이프 누락 · nonce 누락 · prepare() 미사용을 지적하는 규칙이 들어 있고, 편집기와 CI 에 붙여 두면 검토 전에 이미 걸러집니다.
도구가 잡지 못하는 것은 맥락입니다. 이 엔드포인트가 정말 manage_options 여야 하는지, 이 값이 신뢰할 수 있는 출처인지는 사람이 판단합니다. 그래서 자동 검사를 앞에 두고 사람의 주의를 뒤에 남깁니다.
표준을 도구로 강제하는 구체적 설정은 개발 워크플로우 아카이브에서 다루고, 보안 관점의 다른 글은 보안 아카이브에 모여 있습니다. 운영 중인 사이트의 취약점을 진단하고 정리하는 작업은 최적화 지원 사업에 포함돼 있습니다.