정규식 주입 (Regular Expression Injection)

Regular Expression Injection

설명

정규식 주입은 신뢰할 수 없는 데이터가 정규식 패턴의 전부 또는 일부로 해석될 때 발생합니다. 공격자는 .*, 대안(|), 그룹, 수량자 같은 매칭 구문을 주입하여 필터나 검증의 의미를 바꿀 수 있습니다.

백트래킹 방식의 정규식 엔진에서는 조작된 패턴을 충분히 길고 대개 일치하지 않는 대상 문자열에 적용할 때 CPU 사용량이 급증하여 정규식 서비스 거부(ReDoS)가 발생할 수도 있습니다. 단순히 메타문자가 포함되었다는 사실만으로 ReDoS가 성립하는 것은 아니며, 병적인 백트래킹 구조와 이를 유발하는 대상 문자열 조건이 함께 필요합니다.

잠재적 영향

  • 입력 검증 및 필터 우회: 주입된 매칭 구문이 허용하거나 거부하는 문자열 집합을 바꿀 수 있습니다.
  • 서비스 거부(ReDoS): 병적인 백트래킹이 워커의 CPU를 장시간 점유할 수 있습니다.
  • 지연 및 운영 비용 증가: 정규식 처리가 누적되어 응답 지연, 처리량 저하, 오토스케일 비용 증가로 이어질 수 있습니다.

해결 방법

  • 개발자가 관리하는 고정 패턴을 우선 사용하세요. 리터럴 포함 여부나 접두사/접미사 검사는 정규식 대신 in, startswith, endswith 같은 문자열 연산으로 처리하세요.
  • 신뢰할 수 없는 텍스트를 리터럴 패턴 조각으로 사용해야 한다면 해당 조각을 re.escape(...) 또는 regex.escape(...)로 이스케이프하세요. re.escape는 sub 또는 subn의 치환 문자열을 이스케이프하는 함수가 아닙니다.
  • 지원할 검색 패턴 집합이 정해져 있다면 완전하고 신뢰할 수 있는 패턴의 유한 허용 목록을 사용하세요.
  • 사용자가 완전한 정규식을 직접 제공해야 한다면 허용할 문법과 패턴 길이를 명시적으로 제한하고 대상 문자열 길이도 제한하세요. 비백트래킹 엔진이나 엔진이 강제하는 매치 타임아웃을 사용하되, 타임아웃은 정규식 의미 변경에 따른 검증 우회를 막지 못한다는 점에 유의하세요.
  • Python 표준 re API에는 매치별 타임아웃이 없습니다. 서드파티 regex 패키지는 매칭 연산에서 timeout=을 지원합니다. 일반적인 HTTP 요청 타임아웃만으로 CPU를 점유한 정규식 실행이 안전하게 중단된다고 가정하지 마세요.
  • 개발자가 작성한 고정 패턴은 모호한 중첩 수량자 등 치명적 백트래킹을 일으키는 구조가 없는지 별도로 검토하세요.

예시

변경 전

python
import re

from flask import Flask, request

app = Flask(__name__)


@app.get("/search")
def search():
    pattern = request.args.get("p", "")  # 사용자가 패턴 전체를 제어
    text = request.args.get("t", "")
    return "hit" if re.search(pattern, text) else "no"

사용자가 패턴의 의미를 직접 제어합니다. 필터 우회용 패턴뿐 아니라 대상 문자열과 조합했을 때 병적인 백트래킹을 일으키는 패턴도 전달할 수 있습니다.

변경 후

python
from flask import Flask, request, abort

app = Flask(__name__)

MAX_TERM_LENGTH = 100
MAX_TEXT_LENGTH = 10_000


@app.get("/search")
def search():
    term = request.args.get("p", "")
    text = request.args.get("t", "")

    if len(term) > MAX_TERM_LENGTH or len(text) > MAX_TEXT_LENGTH:
        abort(400, "search input too long")

    return "hit" if term in text else "no"

정규식 기능이 필요하지 않으므로 문자열 검색을 사용합니다. 예시의 길이 값은 모든 서비스에 적용되는 보안 상수가 아니라 제품의 입력 정책과 처리 예산에 맞게 정해야 하는 제한값입니다.

고정된 정규식 구조 안에서 사용자 입력을 리터럴로만 사용해야 한다면 입력 조각만 이스케이프합니다.

python
safe_pattern = re.compile(rf"^{re.escape(term)}$")

적용 시 주의사항

패턴, 검색 대상 문자열, sub/subn의 치환 문자열은 서로 다른 역할을 합니다. 입력이 놓이는 위치에 맞게 처리하고, 개발자가 작성한 고정 패턴의 ReDoS 위험도 별도로 검토하세요.

참조