Files
sam-docs/dev/changes/20260315_eval_removal_safe_math_evaluator.md

7.1 KiB

API 보안 개선 — eval() 제거, SafeMathEvaluator 도입

날짜: 2026-03-15 작업자: R&D 개발실장 + Claude Code 배포 대상: API develop 브랜치


변경 개요

API 프로젝트의 보안 감사에서 CRITICAL 등급으로 식별된 eval() 코드 인젝션 취약점 3건을 제거했다. PHP eval() 대신 Shunting-yard 알고리즘 기반의 SafeMathEvaluator 유틸리티를 신규 구현하여 교체했다. 외부 라이브러리 의존 없이 동일한 계산 결과를 보장한다.


1. 왜 이 작업을 했는가 (근거)

1.1 보안 감사 결과

API 코드베이스 전체 보안 감사에서 eval() 사용 3건이 발견되었다.

심각도 파일 위치 용도
CRITICAL Services/Calculation/FormulaParser.php 234행 BOM 산술 수식 계산
CRITICAL Services/Calculation/FormulaParser.php 279행 조건식 비교 평가
CRITICAL Services/Quote/FormulaEvaluatorService.php 316행 견적 수식 계산

1.2 위험성

  • eval()은 문자열을 PHP 코드로 실행하므로 원격 코드 실행(RCE) 위험 존재
  • 기존 코드에 정규식 기반 화이트리스트 필터(isSafeMathExpression)가 있었으나, 정규식 우회 가능성 상존
  • FormulaParser::testFormula() 엔드포인트를 통해 인증된 사용자가 임의 수식을 전달할 수 있는 구조
  • OWASP A03:2021 Injection, CWE-95 Eval Injection 해당

1.3 교체 전략

방안 장점 단점 채택
symfony/expression-language 검증된 라이브러리 외부 의존성 추가, 오버킬
Shunting-yard 직접 구현 의존성 없음, 필요한 기능만 포함 직접 구현 필요
eval() 유지 + 필터 강화 변경 최소 근본적 해결 아님

실제 eval()이 처리하는 연산이 숫자 + 사칙연산 + 비교 연산뿐이므로, 외부 라이브러리 없이 직접 구현이 가장 적합했다.


2. 영향 분석

2.1 eval() 사용 위치 (3곳 모두 private 메서드)

FormulaParser (eval 2곳)
  ← CalculationEngine.calculateBOM()
    ← BomCalculationService
      ← BomCalculationController (설계 BOM 계산)
  ← BomCalculationController.testFormula() (수식 테스트)

FormulaEvaluatorService (eval 1곳)
  ← QuoteCalculationService
    ← QuoteController (견적 자동산출)
  ← Verify5130Calculation (artisan 커맨드)

2.2 영향도

  • 3곳 모두 private 메서드 내부에서만 eval() 사용
  • public 인터페이스(execute(), calculateExpression())의 입출력 시그니처 변경 없음
  • 호출하는 Controller, Service에 수정 불필요
  • 계산 결과 동일 (검증 완료)

3. 수정 내용

3.1 신규 파일

파일 설명
app/Helpers/SafeMathEvaluator.php Shunting-yard 알고리즘 기반 안전한 수식 평가기

SafeMathEvaluator 지원 기능:

기능 메서드 예시
산술 계산 calculate() (3160 * 4350) / 100000013.746
비교 평가 compare() 3000 <= 6000true
단항 마이너스 calculate() -5 + 105
나머지 연산 calculate() 10 % 31
논리 연산 compare() 5 > 3 && 2 < 4true
중첩 괄호 calculate() ((2 + 3) * 4) / 210
0 나누기 방어 calculate() 10 / 0 → 예외 발생

3.2 수정된 파일

파일 변경 내용
app/Services/Calculation/FormulaParser.php eval("return {$expression};") 2곳 → SafeMathEvaluator::calculate(), ::compare()
app/Services/Quote/FormulaEvaluatorService.php eval("return {$expression};") 1곳 → SafeMathEvaluator::calculate()

3.3 변경 전후 비교

FormulaParser::executeSimpleMath() (234행)

// Before
return eval("return {$expression};");

// After
return SafeMathEvaluator::calculate($expression);

FormulaParser::evaluateCondition() (279행)

// Before
return eval("return {$expression};");

// After
return SafeMathEvaluator::compare($expression);

FormulaEvaluatorService::calculateExpression() (316행)

// Before
// TODO: 프로덕션에서는 symfony/expression-language 등 안전한 라이브러리 사용 권장
return (float) eval("return {$expression};");

// After
return \App\Helpers\SafeMathEvaluator::calculate($expression);

4. Shunting-yard 알고리즘 개요

입력: "3160 * 4350 / 1000000"

1. 토큰화
   [3160] [*] [4350] [/] [1000000]

2. 중위 → 후위(RPN) 변환
   [3160] [4350] [*] [1000000] [/]

3. RPN 스택 계산
   3160 * 4350 = 13,746,000
   13,746,000 / 1,000,000 = 13.746

결과: 13.746

연산자 우선순위:

우선순위 연산자
4 단항 마이너스
3 *, /, %
2 +, -

5. 검증 결과

5.1 SafeMathEvaluator 단위 테스트

1) 2+3 = 5                      ✅
2) 10*5-3 = 47                   ✅
3) (2+3)*4 = 20                  ✅
4) 3160*4350/1000000 = 13.746    ✅ (BOM 면적 계산)
5) -5+10 = 5                     ✅ (단항 마이너스)
6) 14.17*3.0 = 42.51             ✅ (소수점)
7) ((2+3)*4)/2 = 10              ✅ (중첩 괄호)
8) 10%3 = 1                      ✅ (나머지)
9) 3000<=6000 = true             ✅ (비교)
10) 7000<=6000 = false           ✅
11) 100==100 = true              ✅
12) 5>3&&2<4 = true              ✅ (논리 AND)

5.2 FormulaParser 통합 테스트

1) screen_size: W1=3160 H1=4350          ✅ (미리 정의된 함수)
2) area: 13.746                           ✅ (산술 수식 → SafeMathEvaluator)
3) bracket_qty (W1=5000): 3               ✅ (조건식 → SafeMathEvaluator)
4) bracket_qty (W1=2000): 2               ✅
5) bracket (predefined, W1=5500): 3       ✅
6) weight: 70.002                         ✅ (중량 계산)

5.3 FormulaEvaluatorService 통합 테스트

1) validateFormula('W * H + 100'): valid      ✅
2) validateFormula(''): invalid                ✅
3) validateFormula('(W + H'): invalid          ✅ (괄호 불일치)

5.4 eval() 잔존 확인

grep -r "eval(" app/  # 결과: 0건 (주석 제외)

테스트 체크리스트

  • SafeMathEvaluator 기본 사칙연산
  • SafeMathEvaluator 비교 연산 + 논리 연산
  • SafeMathEvaluator 단항 마이너스, 소수점, 중첩 괄호
  • FormulaParser 미리 정의된 함수 (eval 미사용 경로)
  • FormulaParser 산술 수식 (eval → SafeMathEvaluator 경로)
  • FormulaParser 조건식 (eval → SafeMathEvaluator 경로)
  • FormulaEvaluatorService 수식 검증
  • app/ 전체 eval() 잔존 검사: 0건
  • Pint 코드 포맷팅 통과

관련 문서


최종 업데이트: 2026-03-15