외부망에서 Base64 PNG를 포함한 POST 요청이 HTTP 응답 없이 종료됐다. WAF가 표준 Base64를 디코딩한 뒤 PNG 압축 데이터의 o'# 바이트를 SQL Injection으로 오탐한 것이 원인이었다. 전송 값을 Base64URL 형식으로 변환한 뒤 정상 동작을 확인했다.

증상
- 이미지가 포함된 보고서 조회 요청에서 브라우저 ERR_EMPTY_RESPONSE가 발생했다.
- HTTP 상태 코드와 응답 본문이 없었고, 연결이 수십 ms 안에 종료됐다.
- 이미지 파라미터를 제외하면 같은 API가 HTTP 200을 반환했다.
- 내부 애플리케이션 로그에는 요청 흔적이 없어 앞단 보안장비를 의심했다.
어떻게 좁혔나
1) 파라미터를 하나씩 제거했다
| 테스트 | 결과 | 판단 |
| 원본 요청 | Empty Response | 재현 |
| Base64 이미지 제거 | HTTP 200 | 이미지 파라미터로 범위 축소 |
| 파일 경로 파라미터 제거·축약 | Empty Response | 경로 파라미터 소거 |
| 동일 길이의 반복 문자열 | HTTP 200 | 단순 길이 제한 소거 |
| 동일 길이의 임의 Base64 | HTTP 200 | Base64 자체가 원인은 아님 |
| 원본 PNG를 Base64URL로 변환 | HTTP 200 및 정상 조회 | 표준 Base64 판별·디코딩 과정이 차단 조건 |
2) Base64를 정렬 상태로 분할했다
Base64는 4글자 단위로 3바이트를 표현한다. 따라서 4글자 경계를 유지한 채 원본 값을 분할하여 호출했다.
- 처음 1,024자는 HTTP 200을 반환했다.
- 1,024자부터 다음 1,024자 구간은 Empty Response가 발생했다.
- 차단 범위를 반복해서 나눈 결과 wKpvJyNe 구간까지 좁혔다.
3) 디코딩 바이트를 변형했다
wKpvJyNe를 Base64 디코딩하면 다음 바이트가 된다.
C0 AA 6F 27 23 5E
o ' # ^
| 변형 | 결과 |
| 원본 o'#^ | Empty Response |
| o 변경 | HTTP 200 |
| 작은따옴표 변경 | HTTP 200 |
| # 변경 | HTTP 200 |
| ^만 변경 | Empty Response |
별도로 o'#^를 Base64로 만든 짧은 값도 동일하게 차단됐다. 감지의 핵심이 o'# 조합임을 확인했다.

원인
WAF가 바이너리를 SQL 문자열로 검사했다
MySQL 계열에서 작은따옴표는 문자열을 종료하고 #은 이후 내용을 주석 처리한다.
SELECT *
FROM some_table
WHERE name = 'o'#' AND enabled = 1;
WAF는 이와 유사한 입력을 SQL Injection 시도로 탐지한다. 이번 요청에서는 사용자가 해당 문자열을 입력하지 않았다. PNG의 압축 데이터 안에 6F 27 23 바이트가 우연히 연속으로 생성됐다.
처리 흐름은 다음과 같았다.
Base64 PNG 전송
→ WAF가 표준 Base64로 판별
→ Base64 디코딩
→ PNG 압축 바이너리를 문자열 규칙으로 검사
→ o'# 패턴 탐지
→ HTTP 응답 없이 연결 종료
PNG를 화면으로 보거나 QR의 실제 내용을 해석하면 o'#는 존재하지 않는다. 압축된 파일 바이트에만 존재하는 오탐이다.
Base64URL은 왜 통과했나
Base64URL은 표준 Base64의 문자를 다음처럼 바꾼다.
| 표준 Base64 | Base64URL |
| + | - |
| / | _ |
| 끝의 = | 일반적으로 생략 |
문제 구간 wKpvJyNe 자체는 변하지 않는다. 그러나 전체 값의 +와 /를 -와 _로 바꾸고 패딩을 제거하면 현재 WAF가 이를 표준 Base64로 판별하지 못해 디코딩을 건너뛴다. 실제로 같은 PNG를 Base64URL 형식으로 전송하자 o'#를 그대로 포함한 데이터임에도 정상 조회됐다. 이 결과로 차단 조건이 PNG 내용 자체가 아니라 표준 Base64 판별 후 수행되는 디코딩 검사임을 확인했다.

해결
운영 보안정책상 특정 URI나 파라미터의 SQLi 검사 예외를 적용하기 어려웠다. 따라서 QR PNG의 전송 형식을 표준 Base64에서 Base64URL로 변경했다.
1) 전송 직전에 Base64URL로 변환했다
클라이언트에서 다음 세 가지 치환을 적용했다.
- +를 -로 변경
- /를 _로 변경
- 끝의 = 패딩 제거
const base64url = base64
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
2) 실제 조회 결과를 확인했다
| 전송 형식 | 결과 |
| 표준 Base64 | Empty Response |
| Base64URL | HTTP 200 및 QR 포함 화면 정상 조회 |
변환 후에도 원본 PNG와 문제 바이트는 동일하다. 달라진 것은 전송 문자열의 표현 방식뿐이다. 따라서 현재 보안장비가 Base64URL 값을 표준 Base64로 인식하지 않아 내부 바이너리 검사를 수행하지 않은 것으로 판단했다.
3) 적용 시 주의사항
Base64URL은 암호화나 보안 기능이 아니다. 수신부에서 Base64URL 디코딩을 지원해야 하며, 인코딩 문자, 최대 길이, PNG 매직 바이트, 이미지 크기를 검증해야 한다. WAF가 향후 Base64URL까지 디코딩하도록 변경되면 같은 오탐이 재발할 수 있다.
장기적으로는 QR 원문이나 식별자만 전송하고 서버에서 QR 이미지를 생성하는 구조가 가장 안정적이다.
정리
- Empty Response가 짧은 시간 안에 발생하고 애플리케이션 로그가 없다면 WAF·프록시 계층부터 확인한다.
- Base64 파라미터는 WAF가 디코딩한 뒤 내부 바이트까지 검사할 수 있다.
- 이미지 압축 바이너리에도 SQLi 문자열과 같은 바이트가 우연히 생길 수 있다.
- 표준 Base64를 Base64URL로 변경해 실제 장애를 해결했으며, 수신부 검증과 장기 구조 개선은 별도로 필요하다.
'Record > Trubble Shooting' 카테고리의 다른 글
| 운영 서버 CPU가 100%로 멈춘 뒤 악성 cron 제거 및 DB 계정 분리한 사례 (0) | 2026.07.09 |
|---|---|
| Windows Cursor 터미널 한글 깨짐, PowerShell 프로필로 해결하는 방법 (0) | 2026.06.21 |
| 싱글톤 Bean 공유 상태로 동시 Ajax 응답이 섞인 사례 (0) | 2026.06.16 |
| 외부망에서만 API가 Empty Response로 끊긴 사례 해결 (0) | 2026.06.16 |
| [Trouble Shooting] 지도 포함 리포트 이미지 삽입 문제 해결 (0) | 2026.02.18 |