RustServer · Security review
메모리 안전성 다음에
확인한 것들.
작은 요청들이 서버를 멈추게 한 이유, 제한 없는 큐가 남긴 비용,
그리고 HTTP 메시지 경계에서 발견한 문제들. RustServer의 보안 진단과 성능을 고려한 수정 방향을 기록합니다.
01 / Context & scope
“내부 메모리 변조나 취약점이 있을까?”
조사의 출발점
CppServer를 Rust로 옮긴 공개 네트워킹 라이브러리의 보안 상태를 확인하는 작업에서 시작했습니다. TCP·TLS·UDP·HTTP·WebSocket·FBE의 파서와 큐, 비동기 작업의 수명, 캐시와 TLS 설정을 살폈습니다.
프로젝트의 forbid(unsafe_code)는 직접 작성한 unsafe 코드를 막습니다. 그러나 큐의 무제한 적재, 콜백 교착, 메시지 경계 혼동은 별도로 검증해야 합니다.
- 기준 커밋
b22031faff65bf078fe8c1e560e94409a049fdfa- 환경
- macOS Apple Silicon / rustc 1.97.1
- 검사 방식
- 소스 검토, loopback 통신, 임시 파일 fixture
- 조사 범위 밖
- 실제 침해 포렌식, 운영 부하 실험, 커널·프로세스 메모리 덤프
02 / Findings
어디서, 왜 문제가 생겼는가
항목을 펼치면 관찰값과 원인, 적용 조건을 확인할 수 있습니다. 9개 항목에 10개 문제 재현이 담겨 있으며, “HTTP 프레이밍”에 두 검사가 포함됩니다.
F01HTTP 파이프라인의 순환 대기
큐의 빈자리와 callback mutex를 서로 기다림
HTTP 파이프라인의 순환 대기
큐의 빈자리와 callback mutex를 서로 기다림수신 콜백은 mutex를 보유한 채 bounded 송신 큐의 빈자리를 기다립니다. writer는 on_sent를 위해 같은 mutex를 기다리느라 다음 항목을 소비하지 못합니다.
클라이언트가 응답을 읽는 상태에서도 재현했습니다. 단순한 느린 수신자 문제와 구분해야 합니다. 정확한 정체 개수는 실행 조건에 따라 달라질 수 있습니다.
소스: TCP read/write loop ↗F02프로토콜 이벤트의 무제한 적재
프레임 상한과 큐의 총 보유량은 다른 제한
프로토콜 이벤트의 무제한 적재
프레임 상한과 큐의 총 보유량은 다른 제한프로토콜 계층의 unbounded_channel이 느린 핸들러 뒤에 계속 이벤트를 쌓을 수 있습니다. 프레임 개수와 총 바이트를 함께 제한할 필요가 있습니다.
연결 콜백을 의도적으로 막은 조건입니다. 수치는 네트워크 수신 통계이며 RSS 측정값이나 실제 OOM의 증거가 아닙니다.
소스: protocol callback queue ↗F03TCP 쓰기 중 종료 지연
시작된 write_all이 취소 신호를 확인하지 않음
TCP 쓰기 중 종료 지연
시작된 write_all이 취소 신호를 확인하지 않음큐 항목 대기에는 취소 경로가 있지만 진행 중인 쓰기에는 없습니다. 종료는 쓰기, 콜백, 남은 큐의 정리까지 포함해 제한해야 합니다.
작은 송신 버퍼를 설정한 TCP 실험입니다. TLS writer에는 이미 쓰기 취소와 shutdown 제한이 있어 같은 누락으로 분류하지 않았습니다. WS는 별도의 코드 검토 대상입니다.
소스: TCP writer ↗F04HTTP timeout에서 빠진 업로드 시간
요청 송신을 완료한 다음에 타이머를 시작
HTTP timeout에서 빠진 업로드 시간
요청 송신을 완료한 다음에 타이머를 시작timeout이 응답 receiver에만 적용됩니다. 송신 잠금과 큐 대기, 쓰기, 응답을 하나의 deadline 안에 넣고 timeout 후 FIFO의 응답 대응도 정리해야 합니다.
재현은 HTTP에서 수행했습니다. HTTPS에는 같은 구조가 소스에 있습니다. 업로드가 느린 정상 사용자도 변경된 timeout의 영향을 받을 수 있습니다.
소스: HTTP request_with_timeout ↗F05출력 직렬화의 CRLF 삽입
입력 파서와 출력 생성자의 검증 수준이 다름
출력 직렬화의 CRLF 삽입
입력 파서와 출력 생성자의 검증 수준이 다름to_bytes()가 문자열을 그대로 이어 붙입니다. 오류를 반환하는 출력 API와 모든 전송 경로의 검증이 필요하며, 기존 API에 우회 경로가 남지 않아야 합니다.
외부 입력이 target이나 header에 도달할 수 있는 애플리케이션이 전제입니다. 모든 사용자 서비스에서 악용 가능하다는 뜻은 아닙니다.
소스: HTTP serialization ↗F06HTTP 프레이밍의 불일치
304 응답 경계와 TE+CL 조합 · 재현 검사 2개
HTTP 프레이밍의 불일치
304 응답 경계와 TE+CL 조합 · 재현 검사 2개상태와 요청 method의 의미를 고려해 본문 길이를 정해야 합니다. HEAD는 요청 문맥이 필요합니다. 지원하지 않는 전송 코딩과 모호한 조합도 일관되게 거부해야 합니다.
304와 TE+CL은 재현했습니다. HEAD·1xx·204는 추가 검사 대상입니다. 프록시를 포함한 request smuggling 공격 사슬 전체를 검증한 결과는 아닙니다.
기준: RFC 9112 §6.3 ↗ · 파서 소스 ↗F07정적 캐시 갱신의 경로 이탈
하위 디렉터리 교체 후 mount 밖 파일을 읽음
정적 캐시 갱신의 경로 이탈
하위 디렉터리 교체 후 mount 밖 파일을 읽음최종 파일의 symlink 여부만으로는 조상 디렉터리의 교체를 막지 못합니다. 검증과 실제 열기 사이의 경쟁까지 포함해 마운트 경계를 유지해야 합니다.
마운트 안 디렉터리를 교체할 로컬 파일시스템 권한이 필요합니다. 원격 URL traversal만으로 재현한 것이 아닙니다.
소스: static cache watchdog ↗F08UDP 유니캐스트의 송신자 경계
설정한 remote와 다른 소켓의 데이터도 콜백으로 전달
UDP 유니캐스트의 송신자 경계
설정한 remote와 다른 소켓의 데이터도 콜백으로 전달recv_from()의 송신자를 remote와 대조하지 않습니다. 유니캐스트의 peer 제한과 멀티캐스트·다중 송신자 허용 정책을 분리할 필요가 있습니다.
여러 송신자를 받으려는 사용법에는 호환성 영향이 있습니다. 주소 검사는 암호학적 인증을 대체하지 않습니다.
소스: UDP receive loop ↗F09미완료 TLS 핸드셰이크의 누적
초기 연결 단계의 deadline과 개수 상한 부재
미완료 TLS 핸드셰이크의 누적
초기 연결 단계의 deadline과 개수 상한 부재소스에서도 서버 핸드셰이크의 별도 timeout과 개수 상한을 확인하지 못했습니다. 전체 연결과 동시 핸드셰이크를 분리해 제한하고 취소 경로를 보장하는 방향입니다.
250ms만으로 영구 잔류나 OOM을 입증한 것은 아닙니다. 관찰과 소스 경로를 함께 근거로 삼았습니다. WS/WSS 초기화는 코드 검토 대상입니다.
소스: TLS connection setup ↗03 / Root cause, visualized
제한된 큐도 설계에 따라 멈출 수 있습니다
HTTP 교착에서 중요한 것은 버퍼 크기보다 잠금과 대기의 순서였습니다.
송신 큐가 가득 참 → 빈자리 대기
같은 mutex 대기 → dequeue 정지
수정 목표: writer의 진행과 콜백 완료 사이의 순환 의존 제거. 별도 큐를 추가해도 동일한 대기 고리를 만들면 해결되지 않습니다.
04 / Patch design
성능을 지키면서 바꿔야 할 것들
설계 예상입니다. 정상 트래픽의 처리량이나 지연이 얼마나 달라지는지는 아직 측정하지 않았습니다.
| 영역 | 비용을 줄이는 방향 | 달라질 수 있는 동작 |
|---|---|---|
| 프로토콜 큐 | 개수 + 총 바이트 예산을 진입 시 관리 | 과부하 시 대기·거부·연결 종료 |
| TCP/WS 종료 | 진행 중 쓰기도 취소, 정리 수명 제한 | 종료 중 미전송 데이터 폐기 가능 |
| HTTP 교착 | writer 진행과 콜백 전달의 순환 대기 제거 | 콜백 시점·합산·순서 계약 검토 |
| TLS/WS 초기화 | 허용 시점에 개수 상한과 deadline 적용 | 초과 연결 거부, 느린 핸드셰이크 timeout |
| HTTP/HTTPS timeout | 요청 전체를 하나의 deadline으로 관리 | 대용량·저속 업로드 실패 시점 변화 |
| TE+CL | 파싱 중 모호한 프레이밍 거부 | 기존 identity 사용과의 호환성 변화 |
| HEAD·1xx·204·304 | 요청 method와 상태로 본문 경계 결정 | 파서 문맥 API 추가 또는 변경 |
| CRLF | 출력 경계의 선형 검증과 오류 반환 | 잘못된 header/target의 호출 실패 |
| 정적 파일 | 마운트·갱신 경로에서 접근 경계 유지 | 디렉터리 교체 시 갱신 거부 가능 |
| UDP | 유니캐스트 peer 제한, 멀티캐스트 분리 | 다중 송신자 사용의 명시적 설정 필요 |
| 메모리 캐시 | 저장 시 총량·엔트리 예산 관리 | 한도 초과 쓰기 실패 또는 축출 |
| 예제 기본값 | loopback 바인딩, 외부 공개는 설정 | 다른 기기의 기본 예제 접속 제한 |
| PEM 파싱 | 기반 라이브러리 API로 의존성 교체 | 인증서 로딩 내부 변경 |
05 / Evidence & limits
테스트 통과가 의미하는 범위를 지킵니다
두 종류의 “통과”
기존 테스트 65개는 기존 동작을 확인합니다. 별도 감사 테스트는 문제 동작 검사 10개 + 입력 변이 검사 1개입니다. 현재 취약 동작을 확인하도록 작성해 일반 실행에서는 제외했습니다.
패치 단계에서는 열 개의 재현 기대값을 바꾸고 일반 회귀 검사에 포함해야 합니다. Criterion 테스트 모드 6개 성공은 성능 측정값이 아닙니다.
알려진 의존성 취약점 0건의 의미
잠금 파일의 의존성 136개, advisory 1,239개를 대조했습니다. DB 갱신일은 2026-09-02입니다. 자체 코드의 결함이나 미공개 취약점까지 없다는 뜻은 아닙니다.
rustls-pemfile 2.2.0 유지보수 중단과 chacha20 0.10.1 yanked 경고는 별도로 기록했습니다. RustSec 안내 ↗
cargo test --locked --test security_audit -- --ignored --nocapture --test-threads=1
cargo test --locked --all-targets --all-features
cargo audit --json
Audit probes 11 passed / 0 failed = 10 problem reproductions + 1 mutation check
Existing tests 65 passed / 0 failed / 11 audit probes ignored
Parser inputs 20,000 deterministic mutations / no panic observed
Dependencies 0 known vulnerability matches / 2 warning entries
감사 명령은 별도 추가한 tests/security_audit.rs가 있는 조사 체크아웃에서 실행했습니다. 이 파일은 기준 커밋에 포함되지 않습니다. 변이 검사는 coverage-guided fuzzing이 아니며 Miri 검증은 완료하지 않았습니다. 정식 CVSS 점수나 CVE도 부여하지 않았습니다.
06 / Next validation
해결됐다고 말하기 전에
- 재현 검사를 차단 기대값으로 바꾸고 HEAD·1xx·204, TLS/WS 수명과 취소 후 자원 회수 검사를 추가합니다.
- 같은 머신과 트래픽으로 패치 전후 TCP·HTTP·TLS·WS의 처리량, 지연, CPU와 RSS를 비교합니다.
- 정상 부하와 과부하를 구분하고 성공·거부·timeout을 함께 보고합니다.
- 느린 상대, 멈춘 콜백, 반복 취소·재연결에서 종료 시간과 남은 task·session을 확인합니다.
- 기존 테스트, fmt, Clippy와 의존성 감사 결과, API·설정의 마이그레이션 내용을 남깁니다.
기록일 2026-09-07 · 이후 패치의 상태는 이 진단 스냅샷과 다를 수 있습니다. 게시일의 원고는 원인과 재현 결과를 기록하며 보안 패치·성능 개선 완료를 주장하지 않습니다.