Post

[Go] 정규식 엔진 RE2와 PCRE의 차이점 - 보안 스캐너 개발 트러블슈팅

Go의 regexp 패키지가 RE2 기반이라 Lookaround를 지원하지 않는 문제와 해결 방법

문제 상황 (Problem)

보안 스캐너를 Go로 개발하던 중, PASSWORD_PLAINTEXT 패턴을 검증하는 과정에서 예상치 못한 에러가 발생했다.

1
2
3
4
5
6
7
// 실패한 정규식 패턴
regex := `(?i)\b(?:password|passwd|pwd|secret)\b\s*[=:]\s*['"]` +
         `((?!(?:test|sample|example|dummy|changeme|todo|placeholder|123456|password))` +
         `[^'"\s]{8,})['"]`

compiled, err := regexp.Compile(regex)
// Error: error parsing regexp: invalid or unsupported Perl syntax: (?!)

에러 메시지

1
[WARN] Failed to compile pattern PASSWORD_PLAINTEXT: error parsing regexp: invalid or unsupported Perl syntax: (?!)

더미 데이터(password="password", secret="test123")를 제외하려고 전방 부정 탐색(Negative Lookahead) (?!...)을 사용했는데, Go의 regexp 패키지에서 지원하지 않는다는 것을 알게 되었다.


원인 분석 (Root Cause)

RE2 vs PCRE

특징RE2 (Go)PCRE (Perl, Python, JavaScript)
엔진Google RE2Perl Compatible Regular Expressions
복잡도O(n) 선형 시간 보장최악의 경우 지수 시간 (Catastrophic Backtracking)
Lookaround❌ 미지원✅ 지원 ((?=...), (?!...), (?<=...), (?<!...))
Backreference❌ 미지원✅ 지원 (\1, \2)
성능빠르고 예측 가능패턴에 따라 느려질 수 있음
보안ReDoS 공격 방어ReDoS 취약

Go가 RE2를 선택한 이유

  1. 성능 보장: 입력 크기에 비례하는 선형 시간 복잡도
  2. 메모리 안정성: 백트래킹이 없어 스택 오버플로우 방지
  3. 보안: ReDoS(Regular Expression Denial of Service) 공격 방어

하지만 이로 인해 LookaroundBackreference 같은 고급 기능을 사용할 수 없다.


해결 방법 (Solution)

1. Lookahead 제거 및 단순화

Lookahead를 사용하지 않고, 패턴을 단순화했다.

1
2
3
4
5
6
7
// Before: PCRE 스타일 (Go에서 작동 안 함)
regex := `(?i)\b(?:password|passwd|pwd|secret)\b\s*[=:]\s*['"]` +
         `((?!(?:test|sample|example|dummy))` + // ❌ Negative Lookahead
         `[^'"\s]{8,})['"]`

// After: RE2 호환 (Go에서 작동)
regex := `(?i)\b(?:password|passwd|pwd|secret)\b\s*[=:]\s*['"]([^'"\s]{8,})['"]`

2. 후처리 필터링 (Post-processing)

정규식으로 넓게 매칭한 후, Go 코드에서 더미 값을 필터링한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var dummyPasswords = []string{
    "test", "sample", "example", "dummy",
    "changeme", "todo", "placeholder",
    "123456", "password",
}

func isDummyPassword(value string) bool {
    lower := strings.ToLower(value)
    for _, dummy := range dummyPasswords {
        if strings.Contains(lower, dummy) {
            return true
        }
    }
    return false
}

// 매칭 후 필터링
if compiled.MatchString(line) {
    match := compiled.FindString(line)
    if !isDummyPassword(match) {
        // 실제 패스워드로 간주
        findings = append(findings, finding)
    }
}

3. 실제 프로젝트 적용 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// scanner.go:54-64
func NewScanner(patterns []Pattern, scanDirs []string, maxSize int, exclude []string) *Scanner {
    s := &Scanner{
        Patterns:        patterns,
        ScanDirectories: scanDirs,
        MaxFileSizeMB:   maxSize,
        ExcludePatterns: exclude,
        compiledPatterns: make(map[string]*regexp.Regexp),
    }

    // Pre-compile regex patterns
    for _, p := range patterns {
        if p.Enabled {
            compiled, err := regexp.Compile(p.Regex)
            if err != nil {
                logger.Warn(fmt.Sprintf("Failed to compile pattern %s: %v", p.ID, err))
                continue // ❌ 에러 발생 시 해당 패턴 건너뛰기
            }
            s.compiledPatterns[p.ID] = compiled
        }
    }

    return s
}

언어별 정규식 엔진 비교

언어엔진LookaroundBackreference특징
GoRE2선형 시간 보장, ReDoS 방어
Pythonre (PCRE)re 모듈은 백트래킹 사용
Pythonre2 (optional)pip install re2 사용 시 RE2 엔진
JavaScriptV8 RegexPCRE 스타일, ES2018부터 Lookbehind 지원
Javajava.util.regexPCRE 스타일
Rustregex crateRE2 기반 (fancy-regex로 Lookaround 가능)
C++std::regexECMAScript, POSIX 등 선택 가능

실전 팁 (Best Practices)

1. 정규식 테스트 시 엔진 확인

Go 정규식을 테스트할 때는 regex101.com에서 Golang flavor를 선택해야 한다.

regex101 Go flavor

2. 복잡한 패턴은 코드로 처리

Lookaround가 필요한 경우:

  • Option 1: 정규식을 단순화하고 Go 코드로 후처리
  • Option 2: github.com/dlclark/regexp2 라이브러리 사용 (PCRE 호환)
1
2
3
4
5
// Option 2: regexp2 사용 (주의: 성능 저하 및 ReDoS 가능)
import "github.com/dlclark/regexp2"

re := regexp2.MustCompile(`(?!test)password`, 0)
isMatch, _ := re.MatchString("password123")

3. 성능 vs 기능 트레이드오프

선택장점단점
RE2 (기본)빠름, 안전Lookaround 불가
regexp2PCRE 호환느림, ReDoS 위험
후처리안전하고 명확코드 길이 증가

대부분의 경우 RE2 + 후처리가 가장 좋은 선택이다.


마치며

Go의 regexp 패키지는 RE2 기반으로 Lookaround를 지원하지 않습니다. 이는 성능과 보안을 위한 의도적인 설계 선택입니다.

Go의 정규식은 RE2를 사용하므로 Lookaround((?=...), (?!...))와 Backreference(\1)를 지원하지 않습니다. 선형 시간 보장을 위해 고급 기능을 희생했지만, 정규식을 단순화하고 Go 코드로 후처리하는 방식으로 해결할 수 있습니다. 정규식 패턴을 작성할 때는 사용하는 언어의 정규식 엔진을 반드시 확인해야 합니다.

보안 스캐너처럼 대량의 파일을 빠르게 스캔해야 하는 경우, RE2의 선형 시간 보장은 큰 장점입니다. Lookaround가 필요한 복잡한 로직은 Go 코드로 명확하게 표현하는 것이 더 나은 선택입니다.

도움이 되셨길 바랍니다! 😀

This post is licensed under CC BY 4.0 by the author.