1use std::borrow::Cow;
7use std::collections::{HashMap, HashSet};
8use std::fmt;
9use std::net::{IpAddr, SocketAddr};
10use std::sync::Arc;
11
12use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
13use httlib_hpack::{Decoder as HpackDecoder, Encoder as HpackEncoder};
14use percent_encoding::percent_decode;
15
16use super::config::SecretsConfigExt;
17use super::hpack::check_header_block;
18use crate::engine::http_deny::{classify_http_request, secret_http_forbidden_response};
19use crate::netstack::shared::SharedState;
20use crate::policy::{EgressEvaluation, HostnameSource, NetworkPolicy, Protocol};
21use crate::secrets::config::{
22 HostPattern, MAX_SECRET_PLACEHOLDER_BYTES, SecretEntry, SecretViolationAction, SecretsConfig,
23};
24
25const MAX_HTTP_HEADER_BYTES: usize = 64 * 1024;
31
32const MAX_HTTP_BODY_BUFFER_BYTES: usize = 16 * 1024 * 1024;
34
35const HTTP2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
37
38const AUTHORIZATION_HEADER_NAME: &[u8] = b"authorization:";
40
41const MAX_HTTP2_FRAME_PAYLOAD_BYTES: usize = 0x00ff_ffff;
45
46const MAX_HTTP2_HEADER_BLOCK_BYTES: usize = 64 * 1024;
48
49const MAX_HTTP2_DECODED_HEADER_BYTES: usize = 64 * 1024;
51
52const MAX_HTTP2_HEADER_FIELDS: usize = 1024;
54
55const MAX_HTTP2_TRACKED_STREAMS: usize = 1024;
57
58const HTTP2_OUTBOUND_FRAME_PAYLOAD_BYTES: usize = 16 * 1024;
61
62const HTTP2_FRAME_DATA: u8 = 0x0;
63const HTTP2_FRAME_HEADERS: u8 = 0x1;
64const HTTP2_FRAME_PUSH_PROMISE: u8 = 0x5;
65const HTTP2_FRAME_CONTINUATION: u8 = 0x9;
66
67const HTTP2_FLAG_END_STREAM: u8 = 0x1;
68const HTTP2_FLAG_END_HEADERS: u8 = 0x4;
69const HTTP2_FLAG_PADDED: u8 = 0x8;
70const HTTP2_FLAG_PRIORITY: u8 = 0x20;
71
72pub struct SecretsHandler {
81 eligible_for_substitution: Vec<EligibleSecret>,
83 ineligible_for_substitution: Vec<IneligibleSecret>,
85 tls_intercepted: bool,
87 sni: String,
89 guest_dst: Option<SocketAddr>,
91 max_detection_window_len: usize,
94 max_body_placeholder_len: usize,
97 placeholder_limit_exceeded: bool,
99 prev_tail: Vec<u8>,
103 http_state: HttpState,
107 http_authority: Option<HttpAuthorityValidator>,
109 opaque: bool,
111 http1_request_summary: Option<RequestSummary>,
113 http_pending: Vec<u8>,
116 unsupported_body_tail: Vec<u8>,
119 http2_state: Option<Http2State>,
121 http1_requests: u8,
123 http1_response_allowed: bool,
125 http1_violation: bool,
127}
128
129#[derive(Debug, Clone)]
131enum HttpState {
132 AwaitingHeaders,
134 InBody { remaining: usize },
137 InChunkedBody { state: ChunkedBodyState },
139 InChunkedRewriteBody { state: ChunkedRewriteState },
142 BufferingBody { remaining: usize },
145}
146
147#[derive(Debug, Clone, Default)]
149struct ChunkedBodyState {
150 phase: ChunkedPhase,
151 line: Vec<u8>,
152 decoded_tail: Vec<u8>,
153}
154
155#[derive(Debug, Clone, Default)]
157struct ChunkedRewriteState {
158 parser: ChunkedBodyState,
159 substitution_tail: Vec<u8>,
160}
161
162struct Http2State {
164 preface_seen: bool,
165 buffer: Vec<u8>,
166 header_block: Option<Http2HeaderBlock>,
167 highest_client_stream_id: u32,
170 open_request_streams: HashSet<u32>,
171 data_tails: HashMap<u32, Vec<u8>>,
172 request_summaries: HashMap<u32, RequestSummary>,
173 decoder: HpackDecoder<'static>,
174 encoder: HpackEncoder<'static>,
175}
176
177struct Http2HeaderBlock {
179 stream_id: u32,
180 end_stream: bool,
181 block: Vec<u8>,
182}
183
184struct Http2Frame<'a> {
186 kind: u8,
187 flags: u8,
188 stream_id: u32,
189 payload: &'a [u8],
190 raw: &'a [u8],
191}
192
193type Http2Headers = Vec<(Vec<u8>, Vec<u8>)>;
194
195#[derive(Debug, Clone, Default)]
197enum ChunkedPhase {
198 #[default]
200 SizeLine,
201 Data { remaining: usize },
203 DataCrlf { seen_cr: bool },
205 TrailerLine,
207}
208
209struct SecretHostIdentity<'a> {
211 guest_ip: IpAddr,
212 shared: &'a SharedState,
213}
214
215#[derive(Clone)]
217enum HttpAuthorityValidator {
218 Sni(String),
220 Policy {
222 guest_dst: SocketAddr,
223 network_policy: Arc<NetworkPolicy>,
224 shared: Arc<SharedState>,
225 secret_host: Option<String>,
228 },
229}
230
231struct HttpRequestMetadata {
233 host_headers: Vec<String>,
234 target_authority: Option<String>,
235}
236
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239enum TransferEncoding {
240 Chunked,
241}
242
243struct RequestFraming {
245 state: HttpState,
246 body_in_request: usize,
247 body_substitution_allowed: bool,
248}
249
250struct ChunkedRewriteResult {
252 output: Vec<u8>,
253 body_end: Option<usize>,
254}
255
256enum ChunkedBodyEvent<'a> {
258 SizeLine(&'a [u8]),
259 Payload(&'a [u8]),
260 ZeroChunk,
261 TrailerLine(&'a [u8]),
262}
263
264struct EligibleSecret {
266 placeholder: String,
267 value: zeroize::Zeroizing<String>,
270 substitute_headers: bool,
271 header_fields: Vec<String>,
273 substitute_query: bool,
274 substitute_body: bool,
275 require_tls_identity: bool,
276}
277
278struct IneligibleSecret {
280 env_var: String,
281 placeholder: String,
282 action: BlockingAction,
283}
284
285struct SecretViolationReport {
287 action: BlockingAction,
288 env_var: String,
289 placeholder: String,
290 protocol: RequestProtocol,
291 location: RequestLocation,
292 match_form: PlaceholderMatchForm,
293 method: Option<String>,
294 path: Option<String>,
295 host: Option<String>,
296 http2_stream_id: Option<u32>,
297}
298
299#[derive(Clone, Default)]
301struct RequestSummary {
302 method: Option<String>,
303 path: Option<String>,
304 host: Option<String>,
305}
306
307#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
309enum BlockingAction {
310 Block,
311 #[default]
312 BlockAndLog,
313 BlockAndTerminate,
314}
315
316#[derive(Debug, Clone, Copy)]
318enum RequestProtocol {
319 Http1,
320 Http2,
321 Opaque,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326enum RequestLocation {
327 Header,
328 Query,
329 BasicAuth,
330 Body,
331 ChunkMetadata,
332 Trailer,
333 Unknown,
334}
335
336#[derive(Debug, Clone, Copy)]
338enum PlaceholderMatchForm {
339 Raw,
340 PercentDecoded,
341 JsonUnescaped,
342 BasicAuthDecoded,
343}
344
345impl EligibleSecret {
350 fn wants_header_injection(&self) -> bool {
353 self.substitute_headers || self.substitute_query
354 }
355
356 fn header_field_allowed(&self, name: &[u8]) -> bool {
359 header_field_allowed(self.substitute_headers, &self.header_fields, name)
360 }
361
362 fn may_substitute_in_headers(&self, headers: &[u8]) -> bool {
365 if !self.wants_header_injection() {
366 return false;
367 }
368
369 let needle = self.placeholder.as_bytes();
370 if (self.substitute_headers || self.substitute_query) && contains_bytes(headers, needle) {
371 return true;
372 }
373
374 if self.substitute_headers {
376 return basic_auth_decoded_contains(
377 String::from_utf8_lossy(headers).as_ref(),
378 &self.placeholder,
379 );
380 }
381
382 false
383 }
384
385 fn substitute_in_headers(&self, headers: &str) -> String {
388 let mut result = String::with_capacity(headers.len());
389 for (i, line) in headers.split("\r\n").enumerate() {
390 if i > 0 {
391 result.push_str("\r\n");
392 }
393 match self.substitute_in_header_line(line, i == 0) {
394 Some(s) => result.push_str(&s),
395 None => result.push_str(line),
396 }
397 }
398 result
399 }
400
401 fn substitute_in_header_line(&self, line: &str, is_request_line: bool) -> Option<String> {
405 if is_request_line {
406 return self
407 .substitute_query
408 .then(|| substitute_query_in_request_line(line, &self.placeholder, &self.value))
409 .flatten();
410 }
411
412 if !self.header_field_allowed(header_field_name(line.as_bytes())) {
413 return None;
414 }
415
416 if is_authorization_header(line)
417 && let Some(replaced) = self.substitute_basic_auth_header(line)
418 {
419 return Some(replaced);
420 }
421 Some(line.replace(&self.placeholder, &self.value))
422 }
423
424 fn substitute_basic_auth_header(&self, line: &str) -> Option<String> {
430 let decoded = decode_basic_credentials(line)?;
431 if !decoded.contains(&self.placeholder) {
432 return None;
433 }
434 let (name, _) = line.split_once(':')?;
435 let replaced = decoded.replace(&self.placeholder, &self.value);
436 Some(format!(
437 "{name}: Basic {}",
438 BASE64.encode(replaced.as_bytes())
439 ))
440 }
441}
442
443impl BlockingAction {
444 fn from_violation_action(action: &SecretViolationAction) -> Option<Self> {
445 match action {
446 SecretViolationAction::Block => Some(Self::Block),
447 SecretViolationAction::BlockAndLog => Some(Self::BlockAndLog),
448 SecretViolationAction::BlockAndTerminate => Some(Self::BlockAndTerminate),
449 }
450 }
451
452 fn into_violation_action(self) -> SecretViolationAction {
453 match self {
454 Self::Block => SecretViolationAction::Block,
455 Self::BlockAndLog => SecretViolationAction::BlockAndLog,
456 Self::BlockAndTerminate => SecretViolationAction::BlockAndTerminate,
457 }
458 }
459}
460
461impl fmt::Display for BlockingAction {
462 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463 let value = match self {
464 Self::Block => "block",
465 Self::BlockAndLog => "block-and-log",
466 Self::BlockAndTerminate => "block-and-terminate",
467 };
468 f.write_str(value)
469 }
470}
471
472impl fmt::Display for RequestProtocol {
473 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474 let value = match self {
475 Self::Http1 => "http/1.1",
476 Self::Http2 => "http/2",
477 Self::Opaque => "opaque",
478 };
479 f.write_str(value)
480 }
481}
482
483impl fmt::Display for RequestLocation {
484 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485 let value = match self {
486 Self::Header => "header",
487 Self::Query => "query",
488 Self::BasicAuth => "authorization_basic",
489 Self::Body => "body",
490 Self::ChunkMetadata => "chunk_metadata",
491 Self::Trailer => "trailer",
492 Self::Unknown => "unknown",
493 };
494 f.write_str(value)
495 }
496}
497
498impl fmt::Display for PlaceholderMatchForm {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 let value = match self {
501 Self::Raw => "raw",
502 Self::PercentDecoded => "percent_decoded",
503 Self::JsonUnescaped => "json_unescaped",
504 Self::BasicAuthDecoded => "basic_auth_decoded",
505 };
506 f.write_str(value)
507 }
508}
509
510impl Default for Http2State {
511 fn default() -> Self {
512 Self {
513 preface_seen: false,
514 buffer: Vec::new(),
515 header_block: None,
516 highest_client_stream_id: 0,
517 open_request_streams: HashSet::new(),
518 data_tails: HashMap::new(),
519 request_summaries: HashMap::new(),
520 decoder: HpackDecoder::with_dynamic_size(4096),
521 encoder: HpackEncoder::with_dynamic_size(4096),
522 }
523 }
524}
525
526impl SecretsHandler {
527 pub fn new(config: &SecretsConfig, sni: &str, tls_intercepted: bool) -> Self {
534 Self::new_inner(config, sni, tls_intercepted, None, None, false)
535 }
536
537 pub fn new_tls_intercepted(
542 config: &SecretsConfig,
543 sni: &str,
544 guest_ip: IpAddr,
545 shared: &SharedState,
546 ) -> Self {
547 Self::new_inner(
548 config,
549 sni,
550 true,
551 Some(SecretHostIdentity { guest_ip, shared }),
552 Some(HttpAuthorityValidator::Sni(sni.to_string())),
553 false,
554 )
555 }
556
557 pub(crate) fn new_tls_intercepted_via_connect(config: &SecretsConfig, sni: &str) -> Self {
562 Self::new_inner(
563 config,
564 sni,
565 true,
566 None,
567 Some(HttpAuthorityValidator::Sni(sni.to_string())),
568 false,
569 )
570 }
571
572 pub fn new_plain_http(
577 config: &SecretsConfig,
578 host: &str,
579 guest_ip: IpAddr,
580 shared: &SharedState,
581 ) -> Self {
582 Self::new_inner(
583 config,
584 host,
585 false,
586 Some(SecretHostIdentity { guest_ip, shared }),
587 Some(HttpAuthorityValidator::Sni(host.to_string())),
588 false,
589 )
590 }
591
592 pub(crate) fn new_plain_http_policy(
594 config: &SecretsConfig,
595 host: &str,
596 guest_dst: SocketAddr,
597 network_policy: Arc<NetworkPolicy>,
598 shared: Arc<SharedState>,
599 ) -> Self {
600 let identity = (!host.is_empty()).then_some(SecretHostIdentity {
601 guest_ip: guest_dst.ip(),
602 shared: shared.as_ref(),
603 });
604 Self::new_inner(
605 config,
606 host,
607 false,
608 identity,
609 Some(HttpAuthorityValidator::Policy {
610 guest_dst,
611 network_policy,
612 shared: shared.clone(),
613 secret_host: config.has_host_scoped_secrets().then(|| host.to_string()),
614 }),
615 false,
616 )
617 }
618
619 pub fn new_plain_http_invalid_host(config: &SecretsConfig) -> Self {
624 let host_scoped = config
625 .secrets
626 .iter()
627 .any(|secret| secret.allowed_hosts.iter().any(|h| *h != HostPattern::Any));
628
629 Self::new_inner(config, "", false, None, None, host_scoped)
630 }
631
632 pub(crate) fn new_plain_http_untrusted_metadata(config: &SecretsConfig) -> Self {
638 Self::new_inner(config, "", false, None, None, true)
639 }
640
641 fn new_inner(
642 config: &SecretsConfig,
643 sni: &str,
644 tls_intercepted: bool,
645 identity: Option<SecretHostIdentity<'_>>,
646 http_authority: Option<HttpAuthorityValidator>,
647 force_ineligible: bool,
648 ) -> Self {
649 let mut eligible_for_substitution = Vec::new();
650 let mut ineligible_for_substitution = Vec::new();
651 let mut max_detection_window_len = 0;
652 let mut max_body_placeholder_len = 0;
653 let mut placeholder_limit_exceeded = false;
654
655 for secret in &config.secrets {
656 if secret.placeholder.len() > MAX_SECRET_PLACEHOLDER_BYTES {
657 placeholder_limit_exceeded = true;
658 }
659 max_detection_window_len = max_detection_window_len.max(max_placeholder_detection_len(
660 secret.placeholder.len().min(MAX_SECRET_PLACEHOLDER_BYTES),
661 ));
662
663 let host_allowed =
664 !force_ineligible && secret_host_allowed(secret, sni, identity.as_ref());
665
666 if host_allowed {
669 if secret.substitution.body {
670 max_body_placeholder_len = max_body_placeholder_len
671 .max(secret.placeholder.len().min(MAX_SECRET_PLACEHOLDER_BYTES));
672 }
673 eligible_for_substitution.push(EligibleSecret {
674 placeholder: secret.placeholder.clone(),
675 value: secret.value.clone(),
676 substitute_headers: secret.substitution.headers,
677 header_fields: secret.substitution.header_fields.clone(),
678 substitute_query: secret.substitution.query,
679 substitute_body: secret.substitution.body,
680 require_tls_identity: secret.require_tls_identity,
681 });
682 if !secret.require_tls_identity || tls_intercepted {
683 continue;
684 }
685 }
686
687 if secret
688 .passthrough_hosts
689 .iter()
690 .any(|pattern| host_pattern_allowed(pattern, sni, identity.as_ref()))
691 {
692 continue;
693 }
694
695 if secret.violation_action.is_none()
698 && config.passthrough_hosts.as_ref().is_some_and(|hosts| {
699 hosts
700 .iter()
701 .any(|pattern| host_pattern_allowed(pattern, sni, identity.as_ref()))
702 })
703 {
704 continue;
705 }
706
707 let action = secret
708 .violation_action
709 .as_ref()
710 .unwrap_or(&config.violation_action);
711 ineligible_for_substitution.push(IneligibleSecret {
712 env_var: secret.env_var.clone(),
713 placeholder: secret.placeholder.clone(),
714 action: BlockingAction::from_violation_action(action).unwrap_or_default(),
715 });
716 }
717
718 ineligible_for_substitution.retain(|ineligible| {
721 !eligible_for_substitution.iter().any(|eligible| {
722 ineligible.placeholder == eligible.placeholder
723 && (!eligible.require_tls_identity || tls_intercepted)
724 })
725 });
726
727 Self {
728 eligible_for_substitution,
729 ineligible_for_substitution,
730 tls_intercepted,
731 sni: sni.to_string(),
732 guest_dst: None,
733 max_detection_window_len,
734 max_body_placeholder_len,
735 placeholder_limit_exceeded,
736 prev_tail: Vec::new(),
737 http_state: HttpState::AwaitingHeaders,
738 http_authority,
739 opaque: false,
740 http1_request_summary: None,
741 http_pending: Vec::new(),
742 unsupported_body_tail: Vec::new(),
743 http2_state: None,
744 http1_requests: 0,
745 http1_response_allowed: false,
746 http1_violation: false,
747 }
748 }
749
750 pub fn with_guest_dst(mut self, guest_dst: SocketAddr) -> Self {
752 self.guest_dst = Some(guest_dst);
753 self
754 }
755
756 pub fn substitute<'a>(
767 &mut self,
768 data: &'a [u8],
769 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
770 if self.placeholder_limit_exceeded {
771 tracing::error!(
772 "secret configuration rejected: placeholder exceeds {} bytes",
773 MAX_SECRET_PLACEHOLDER_BYTES
774 );
775 return Err(SecretViolationAction::Block);
776 }
777
778 if self.opaque {
779 return self.scan_opaque(data);
780 }
781
782 if self.http2_state.is_some() {
783 return self.substitute_http2(data);
784 }
785
786 match std::mem::replace(&mut self.http_state, HttpState::AwaitingHeaders) {
789 HttpState::BufferingBody { remaining } => {
790 return self.substitute_buffered_body(data, remaining);
791 }
792 HttpState::InBody { remaining } => {
793 return self.substitute_body_chunk(data, remaining);
794 }
795 HttpState::InChunkedBody { state } => {
796 return self.substitute_chunked_body_chunk(data, state);
797 }
798 HttpState::InChunkedRewriteBody { state } => {
799 return self.substitute_chunked_rewrite_body_chunk(data, state);
800 }
801 HttpState::AwaitingHeaders => {}
802 }
803
804 if self.http_pending.is_empty() {
805 if has_complete_http2_preface(data) {
806 self.http2_state = Some(Http2State::default());
807 return self.substitute_http2(data);
808 }
809 if is_http2_preface_prefix(data) {
810 self.http_pending.extend_from_slice(data);
811 return Ok(Cow::Owned(Vec::new()));
812 }
813 } else {
814 let mut pending_prefix = Vec::with_capacity(self.http_pending.len() + data.len());
815 pending_prefix.extend_from_slice(&self.http_pending);
816 pending_prefix.extend_from_slice(data);
817 if has_complete_http2_preface(&pending_prefix) {
818 self.http_pending.clear();
819 self.http2_state = Some(Http2State::default());
820 return self.substitute_http2(&pending_prefix);
821 }
822 if is_http2_preface_prefix(&pending_prefix) {
823 self.http_pending = pending_prefix;
824 return Ok(Cow::Owned(Vec::new()));
825 }
826 }
827
828 if !self.http_pending.is_empty() {
829 self.http_pending.extend_from_slice(data);
830 let header_boundary = find_header_boundary(&self.http_pending);
831 if http_request_line_has_binary_control(&self.http_pending) {
832 let pending = std::mem::take(&mut self.http_pending);
833 let output = self.scan_opaque(&pending)?.into_owned();
834 return Ok(Cow::Owned(output));
835 }
836 if self.http_pending.len() > MAX_HTTP_HEADER_BYTES {
837 return Err(SecretViolationAction::Block);
838 }
839 if header_boundary.is_none() {
840 if first_line_is_not_http_request(&self.http_pending)
841 || !looks_like_http_request_prefix(&self.http_pending)
842 {
843 let pending = std::mem::take(&mut self.http_pending);
844 let output = self.substitute_headers(&pending)?.into_owned();
845 return Ok(Cow::Owned(output));
846 }
847 return Ok(Cow::Owned(Vec::new()));
848 }
849
850 let pending = std::mem::take(&mut self.http_pending);
851 let output = self.substitute_headers(&pending)?.into_owned();
852 return Ok(Cow::Owned(output));
853 }
854
855 if http_request_line_has_binary_control(data) {
856 return self.scan_opaque(data);
857 }
858
859 if find_header_boundary(data).is_none()
860 && looks_like_http_request_prefix(data)
861 && !first_line_is_not_http_request(data)
862 {
863 if data.len() > MAX_HTTP_HEADER_BYTES {
864 return Err(SecretViolationAction::Block);
865 }
866 self.http_pending.extend_from_slice(data);
867 return Ok(Cow::Owned(Vec::new()));
868 }
869
870 self.substitute_headers(data)
871 }
872
873 pub(crate) fn http1_violation_response(&self, message: &str) -> Option<Vec<u8>> {
876 (self.http1_response_allowed && self.http1_violation)
877 .then(|| secret_http_forbidden_response(message, &self.sni))
878 }
879
880 fn scan_opaque<'a>(&mut self, data: &'a [u8]) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
881 if !self.opaque && self.http_authority.is_some() && opaque_prefix_might_be_http(data) {
885 return Err(SecretViolationAction::Block);
886 }
887 self.opaque = true;
888 let report = detect_blocking_action_with_tail(
889 &self.ineligible_for_substitution,
890 &self.prev_tail,
891 data,
892 "",
893 RequestProtocol::Opaque,
894 RequestLocation::Unknown,
895 None,
896 );
897 self.apply_blocking_action(report)?;
898 self.update_opaque_tail(data);
899 Ok(Cow::Borrowed(data))
900 }
901
902 fn update_opaque_tail(&mut self, data: &[u8]) {
903 let mut scan = Vec::with_capacity(self.prev_tail.len() + data.len());
904 scan.extend_from_slice(&self.prev_tail);
905 scan.extend_from_slice(data);
906
907 let base_len = self
908 .max_detection_window_len
909 .max(AUTHORIZATION_HEADER_NAME.len() + 2)
910 .saturating_sub(1);
911 let authorization_line = scan
912 .windows(AUTHORIZATION_HEADER_NAME.len())
913 .rposition(|window| window.eq_ignore_ascii_case(AUTHORIZATION_HEADER_NAME))
914 .and_then(|start| {
915 let line = &scan[start..];
916 (!line.windows(2).any(|window| window == b"\r\n")).then_some(line)
917 });
918
919 if let Some(line) = authorization_line
920 && line.len() > MAX_HTTP_HEADER_BYTES
921 && let Some(encoded) = opaque_basic_auth_payload(line)
922 {
923 let mut suffix_start = encoded.len().saturating_sub(base_len);
924 suffix_start -= suffix_start % 4;
925 self.prev_tail.clear();
926 self.prev_tail.extend_from_slice(AUTHORIZATION_HEADER_NAME);
927 self.prev_tail.extend_from_slice(b" Basic ");
928 self.prev_tail.extend_from_slice(&encoded[suffix_start..]);
929 return;
930 }
931
932 let tail_len = authorization_line
933 .filter(|line| line.len() <= MAX_HTTP_HEADER_BYTES)
934 .map_or(base_len, <[u8]>::len);
935 update_tail_buffer(&mut self.prev_tail, data, tail_len.max(base_len));
936 }
937
938 fn substitute_http2<'a>(
939 &mut self,
940 data: &[u8],
941 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
942 let mut state = self.http2_state.take().unwrap_or_default();
943 let output = state.process(self, data)?;
944 self.http2_state = Some(state);
945 Ok(Cow::Owned(output))
946 }
947
948 fn substitute_headers<'a>(
949 &mut self,
950 data: &'a [u8],
951 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
952 self.http1_requests = self.http1_requests.saturating_add(1);
953 self.http1_response_allowed =
954 self.http1_requests == 1 && classify_http_request(data) == Some(true);
955 self.substitute_ready(data)
956 }
957
958 fn substitute_ready<'a>(
959 &mut self,
960 data: &'a [u8],
961 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
962 let boundary = find_header_boundary(data);
965 let (header_bytes, after_headers) = match boundary {
966 Some(pos) => (&data[..pos], &data[pos..]),
967 None => (data, &[] as &[u8]),
968 };
969
970 let mut body_substitution_allowed = false;
976 let (body_bytes, spillover) = if boundary.is_some() {
977 let header_text = String::from_utf8_lossy(header_bytes);
978 let request_summary = http1_request_summary(header_text.as_ref());
979 if let Some(validator) = self.http_authority.as_ref()
980 && let Some(metadata) = parse_http_request_metadata(header_bytes)?
981 {
982 validate_http1_authority(&metadata, validator)?;
983 }
984
985 let transfer_encoding = parse_transfer_encoding(header_text.as_ref())?;
986 if transfer_encoding.is_some() && parse_content_length(header_text.as_ref())?.is_some()
987 {
988 return Err(SecretViolationAction::Block);
989 }
990
991 if transfer_encoding == Some(TransferEncoding::Chunked) {
992 return self.substitute_chunked_ready(
993 data,
994 header_bytes,
995 after_headers,
996 header_text.as_ref(),
997 );
998 }
999
1000 let framing = next_state_after_headers(header_text.as_ref(), after_headers)?;
1001 if self.needs_body_substitution()
1002 && framing.body_substitution_allowed
1003 && content_length_exceeds_buffer_limit(header_text.as_ref())?
1004 {
1005 return Err(SecretViolationAction::Block);
1006 }
1007 if self.needs_body_substitution()
1008 && framing.body_substitution_allowed
1009 && let HttpState::InBody { remaining } = &framing.state
1010 {
1011 self.http_pending.extend_from_slice(data);
1012 self.http1_request_summary = Some(request_summary);
1013 self.http_state = HttpState::BufferingBody {
1014 remaining: *remaining,
1015 };
1016 return Ok(Cow::Owned(Vec::new()));
1017 }
1018
1019 body_substitution_allowed = framing.body_substitution_allowed;
1020 self.http_state = framing.state;
1021 self.http1_request_summary = if matches!(self.http_state, HttpState::InBody { .. }) {
1022 Some(request_summary)
1023 } else {
1024 None
1025 };
1026 after_headers.split_at(framing.body_in_request)
1027 } else {
1028 (after_headers, &[] as &[u8])
1029 };
1030
1031 let this_request = &data[..header_bytes.len() + body_bytes.len()];
1033
1034 self.apply_blocking_action(self.detect_blocking_action(
1036 this_request,
1037 String::from_utf8_lossy(header_bytes).as_ref(),
1038 RequestLocation::Unknown,
1039 ))?;
1040 if !body_substitution_allowed {
1041 self.block_unsupported_body_placeholder(&self.unsupported_body_tail, body_bytes)?;
1042 if matches!(self.http_state, HttpState::InBody { .. }) {
1043 update_tail_buffer(
1044 &mut self.unsupported_body_tail,
1045 body_bytes,
1046 self.max_body_placeholder_len.saturating_sub(1),
1047 );
1048 } else {
1049 self.unsupported_body_tail.clear();
1050 }
1051 } else {
1052 self.unsupported_body_tail.clear();
1053 }
1054 if matches!(self.http_state, HttpState::InBody { .. }) {
1055 self.update_tail(body_bytes);
1056 } else {
1057 self.prev_tail.clear();
1060 }
1061
1062 if self.eligible_for_substitution.is_empty() {
1063 return self.append_pipelined_spillover(data, this_request, spillover);
1066 }
1067
1068 let mut header_str = None;
1070 let mut body = None;
1071
1072 for secret in &self.eligible_for_substitution {
1073 if secret.require_tls_identity && !self.tls_intercepted {
1075 continue;
1076 }
1077
1078 if secret.may_substitute_in_headers(header_bytes) {
1080 let current = header_str
1081 .get_or_insert_with(|| String::from_utf8_lossy(header_bytes).into_owned());
1082 *current = secret.substitute_in_headers(current);
1083 }
1084
1085 if body_substitution_allowed && secret.substitute_body {
1087 let source = body.as_deref().unwrap_or(body_bytes);
1088 if let Some(replaced) = replace_bytes(
1089 source,
1090 secret.placeholder.as_bytes(),
1091 secret.value.as_bytes(),
1092 ) {
1093 body = Some(replaced);
1094 }
1095 }
1096 }
1097
1098 let header_changed = header_str
1099 .as_ref()
1100 .is_some_and(|headers| headers.as_bytes() != header_bytes);
1101 let body_changed = body.is_some();
1102
1103 if !header_changed && !body_changed {
1106 return self.append_pipelined_spillover(data, this_request, spillover);
1107 }
1108
1109 let header_len = header_str
1110 .as_ref()
1111 .map_or(header_bytes.len(), |headers| headers.len());
1112 let body_len = body.as_ref().map_or(body_bytes.len(), Vec::len);
1113 let mut output = Vec::with_capacity(header_len + body_len + spillover.len());
1114
1115 let body_bytes_out = body.as_deref().unwrap_or(body_bytes);
1116 if body_changed && body_bytes_out.len() != body_bytes.len() {
1118 let headers = match header_str {
1119 Some(headers) => update_content_length(&headers, body_bytes_out.len()),
1120 None => update_content_length(
1121 String::from_utf8_lossy(header_bytes).as_ref(),
1122 body_bytes_out.len(),
1123 ),
1124 };
1125 output.extend_from_slice(headers.as_bytes());
1126 } else if let Some(headers) = header_str {
1127 output.extend_from_slice(headers.as_bytes());
1128 } else {
1129 output.extend_from_slice(header_bytes);
1130 }
1131
1132 output.extend_from_slice(body_bytes_out);
1133
1134 if !spillover.is_empty() {
1135 let next_out = self.substitute(spillover)?;
1136 output.extend_from_slice(next_out.as_ref());
1137 }
1138 Ok(Cow::Owned(output))
1139 }
1140
1141 fn substitute_buffered_body<'a>(
1142 &mut self,
1143 data: &'a [u8],
1144 remaining: usize,
1145 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
1146 let take = remaining.min(data.len());
1147 self.http_pending.extend_from_slice(&data[..take]);
1148
1149 if take < remaining {
1150 self.http_state = HttpState::BufferingBody {
1151 remaining: remaining - take,
1152 };
1153 return Ok(Cow::Owned(Vec::new()));
1154 }
1155
1156 self.http_state = HttpState::AwaitingHeaders;
1157 let request = std::mem::take(&mut self.http_pending);
1158 let mut output = self.substitute_ready(&request)?.into_owned();
1159
1160 if data.len() > take {
1161 let spillover = self.substitute(&data[take..])?;
1162 output.extend_from_slice(spillover.as_ref());
1163 }
1164
1165 Ok(Cow::Owned(output))
1166 }
1167
1168 fn append_pipelined_spillover<'a>(
1173 &mut self,
1174 parent: &'a [u8],
1175 this_request: &'a [u8],
1176 spillover: &'a [u8],
1177 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
1178 if spillover.is_empty() {
1179 return Ok(Cow::Borrowed(parent));
1180 }
1181 let next_out = self.substitute(spillover)?;
1182 if let Cow::Borrowed(b) = &next_out
1183 && std::ptr::eq(b.as_ptr(), spillover.as_ptr())
1184 && b.len() == spillover.len()
1185 {
1186 return Ok(Cow::Borrowed(parent));
1190 }
1191 let next_bytes = next_out.as_ref();
1192 let mut out = Vec::with_capacity(this_request.len() + next_bytes.len());
1193 out.extend_from_slice(this_request);
1194 out.extend_from_slice(next_bytes);
1195 Ok(Cow::Owned(out))
1196 }
1197
1198 fn substitute_chunked_ready<'a>(
1200 &mut self,
1201 parent: &'a [u8],
1202 header_bytes: &'a [u8],
1203 after_headers: &'a [u8],
1204 headers: &str,
1205 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
1206 if self.needs_body_substitution() && !has_non_identity_content_encoding(headers) {
1207 return self.substitute_chunked_rewrite_ready(header_bytes, after_headers, headers);
1208 }
1209
1210 self.http1_request_summary = Some(http1_request_summary(headers));
1214 self.apply_blocking_action(self.detect_http1_fragment_blocking_action(
1215 &[],
1216 header_bytes,
1217 headers,
1218 RequestLocation::Unknown,
1219 ))?;
1220 self.prev_tail.clear();
1221
1222 let mut state = ChunkedBodyState::default();
1223 let body_end =
1224 self.consume_chunked_body_with_violation_detection(&mut state, after_headers)?;
1225 let (body_part, spillover) = match body_end {
1226 Some(end) => after_headers.split_at(end),
1227 None => (after_headers, &[] as &[u8]),
1228 };
1229 let this_request = &parent[..header_bytes.len() + body_part.len()];
1230
1231 self.http_state = if body_end.is_some() {
1232 self.http1_request_summary = None;
1233 HttpState::AwaitingHeaders
1234 } else {
1235 HttpState::InChunkedBody { state }
1236 };
1237
1238 if let Some(headers) = self.substitute_header_bytes(header_bytes) {
1239 let mut output = Vec::with_capacity(headers.len() + body_part.len() + spillover.len());
1240 output.extend_from_slice(headers.as_bytes());
1241 output.extend_from_slice(body_part);
1242 if !spillover.is_empty() {
1243 let next_out = self.substitute(spillover)?;
1244 output.extend_from_slice(next_out.as_ref());
1245 }
1246 return Ok(Cow::Owned(output));
1247 }
1248
1249 self.append_pipelined_spillover(parent, this_request, spillover)
1250 }
1251
1252 fn substitute_chunked_rewrite_ready<'a>(
1254 &mut self,
1255 header_bytes: &'a [u8],
1256 after_headers: &'a [u8],
1257 headers: &str,
1258 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
1259 self.http1_request_summary = Some(http1_request_summary(headers));
1263 self.apply_blocking_action(self.detect_http1_fragment_blocking_action(
1264 &[],
1265 header_bytes,
1266 headers,
1267 RequestLocation::Unknown,
1268 ))?;
1269 self.prev_tail.clear();
1270
1271 let mut state = ChunkedRewriteState::default();
1272 let rewrite = self.rewrite_chunked_body_part(&mut state, after_headers)?;
1273 let spillover = match rewrite.body_end {
1274 Some(end) => &after_headers[end..],
1275 None => &[] as &[u8],
1276 };
1277
1278 self.http_state = if rewrite.body_end.is_some() {
1279 self.http1_request_summary = None;
1280 HttpState::AwaitingHeaders
1281 } else {
1282 HttpState::InChunkedRewriteBody { state }
1283 };
1284
1285 let header_len = header_bytes.len();
1286 let header_out = self.substitute_header_bytes(header_bytes);
1287 let mut output = Vec::with_capacity(
1288 header_out
1289 .as_ref()
1290 .map_or(header_len, |headers| headers.len())
1291 + rewrite.output.len()
1292 + spillover.len(),
1293 );
1294 if let Some(headers) = header_out {
1295 output.extend_from_slice(headers.as_bytes());
1296 } else {
1297 output.extend_from_slice(header_bytes);
1298 }
1299 output.extend_from_slice(&rewrite.output);
1300
1301 if !spillover.is_empty() {
1302 let next_out = self.substitute(spillover)?;
1303 output.extend_from_slice(next_out.as_ref());
1304 }
1305
1306 Ok(Cow::Owned(output))
1307 }
1308
1309 fn substitute_body_chunk<'a>(
1319 &mut self,
1320 data: &'a [u8],
1321 remaining: usize,
1322 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
1323 let body_end = (data.len() >= remaining).then_some(remaining);
1328 let (body_part, spillover) = match body_end {
1329 Some(end) => data.split_at(end),
1330 None => (data, &[] as &[u8]),
1331 };
1332
1333 self.block_unsupported_body_placeholder(&self.unsupported_body_tail, body_part)?;
1334 self.apply_blocking_action(self.detect_blocking_action(
1335 body_part,
1336 "",
1337 RequestLocation::Body,
1338 ))?;
1339 if body_end.is_some() {
1340 self.prev_tail.clear();
1341 } else {
1342 self.update_tail(body_part);
1343 }
1344
1345 self.http_state = match body_end {
1348 Some(_) => {
1349 self.http1_request_summary = None;
1350 self.unsupported_body_tail.clear();
1351 HttpState::AwaitingHeaders
1352 }
1353 None => {
1354 update_tail_buffer(
1355 &mut self.unsupported_body_tail,
1356 body_part,
1357 self.max_body_placeholder_len.saturating_sub(1),
1358 );
1359 HttpState::InBody {
1360 remaining: remaining - body_part.len(),
1361 }
1362 }
1363 };
1364
1365 self.append_pipelined_spillover(data, body_part, spillover)
1366 }
1367
1368 fn substitute_chunked_body_chunk<'a>(
1370 &mut self,
1371 data: &'a [u8],
1372 mut state: ChunkedBodyState,
1373 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
1374 let body_end = self.consume_chunked_body_with_violation_detection(&mut state, data)?;
1375 let (body_part, spillover) = match body_end {
1376 Some(end) => data.split_at(end),
1377 None => (data, &[] as &[u8]),
1378 };
1379
1380 self.http_state = if body_end.is_some() {
1381 self.http1_request_summary = None;
1382 HttpState::AwaitingHeaders
1383 } else {
1384 HttpState::InChunkedBody { state }
1385 };
1386
1387 self.append_pipelined_spillover(data, body_part, spillover)
1388 }
1389
1390 fn substitute_chunked_rewrite_body_chunk<'a>(
1393 &mut self,
1394 data: &'a [u8],
1395 mut state: ChunkedRewriteState,
1396 ) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
1397 let rewrite = self.rewrite_chunked_body_part(&mut state, data)?;
1398 let spillover = match rewrite.body_end {
1399 Some(end) => &data[end..],
1400 None => &[] as &[u8],
1401 };
1402
1403 self.http_state = if rewrite.body_end.is_some() {
1404 self.http1_request_summary = None;
1405 HttpState::AwaitingHeaders
1406 } else {
1407 HttpState::InChunkedRewriteBody { state }
1408 };
1409
1410 let mut output = rewrite.output;
1411 if !spillover.is_empty() {
1412 let next_out = self.substitute(spillover)?;
1413 output.extend_from_slice(next_out.as_ref());
1414 }
1415
1416 Ok(Cow::Owned(output))
1417 }
1418
1419 pub fn is_empty(&self) -> bool {
1421 self.http_authority.is_none()
1422 && self.http_pending.is_empty()
1423 && self.unsupported_body_tail.is_empty()
1424 && self.http1_request_summary.is_none()
1425 && self.http2_state.is_none()
1426 && matches!(self.http_state, HttpState::AwaitingHeaders)
1427 && self.eligible_for_substitution.is_empty()
1428 && self.ineligible_for_substitution.is_empty()
1429 }
1430
1431 fn needs_body_substitution(&self) -> bool {
1432 self.eligible_for_substitution.iter().any(|secret| {
1433 secret.substitute_body && (!secret.require_tls_identity || self.tls_intercepted)
1434 })
1435 }
1436
1437 fn block_unsupported_body_placeholder(
1438 &self,
1439 prev_tail: &[u8],
1440 data: &[u8],
1441 ) -> Result<(), SecretViolationAction> {
1442 if self.contains_eligible_body_placeholder(prev_tail, data) {
1443 tracing::warn!(
1444 "secret substitution in this request body is unsupported; blocking placeholder"
1445 );
1446 return Err(SecretViolationAction::Block);
1447 }
1448 Ok(())
1449 }
1450
1451 fn contains_eligible_body_placeholder(&self, prev_tail: &[u8], data: &[u8]) -> bool {
1452 if !self.needs_body_substitution() {
1453 return false;
1454 }
1455
1456 let scan_buf: Cow<[u8]> = if prev_tail.is_empty() {
1457 Cow::Borrowed(data)
1458 } else {
1459 let mut stitched = Vec::with_capacity(prev_tail.len() + data.len());
1460 stitched.extend_from_slice(prev_tail);
1461 stitched.extend_from_slice(data);
1462 Cow::Owned(stitched)
1463 };
1464 let scan = scan_buf.as_ref();
1465 self.eligible_for_substitution.iter().any(|secret| {
1466 secret.substitute_body
1467 && !secret.placeholder.is_empty()
1468 && (!secret.require_tls_identity || self.tls_intercepted)
1469 && contains_bytes(scan, secret.placeholder.as_bytes())
1470 })
1471 }
1472
1473 fn substitute_http2_headers(&self, headers: &mut [(Vec<u8>, Vec<u8>)]) {
1474 for secret in &self.eligible_for_substitution {
1475 if secret.require_tls_identity && !self.tls_intercepted {
1476 continue;
1477 }
1478
1479 for (name, value) in headers.iter_mut() {
1480 let is_pseudo = name.starts_with(b":");
1481 let header_allowed = !is_pseudo && secret.header_field_allowed(name);
1482
1483 if name.eq_ignore_ascii_case(b":path")
1484 && secret.substitute_query
1485 && let Ok(path) = std::str::from_utf8(value)
1486 && let Some(replaced) =
1487 substitute_query_in_target(path, &secret.placeholder, &secret.value)
1488 {
1489 *value = replaced.into_bytes();
1490 }
1491
1492 if header_allowed
1493 && name.eq_ignore_ascii_case(b"authorization")
1494 && let Ok(header_value) = std::str::from_utf8(value)
1495 && let Some(replaced) = substitute_basic_auth_value(
1496 header_value,
1497 &secret.placeholder,
1498 &secret.value,
1499 )
1500 {
1501 *value = replaced.into_bytes();
1502 }
1503
1504 if header_allowed && contains_bytes(value, secret.placeholder.as_bytes()) {
1505 let replaced =
1506 String::from_utf8_lossy(value).replace(&secret.placeholder, &secret.value);
1507 *value = replaced.into_bytes();
1508 }
1509 }
1510 }
1511 }
1512
1513 fn substitute_header_bytes(&self, header_bytes: &[u8]) -> Option<String> {
1514 let mut header_str: Option<String> = None;
1515 for secret in &self.eligible_for_substitution {
1516 if secret.require_tls_identity && !self.tls_intercepted {
1517 continue;
1518 }
1519 if secret.may_substitute_in_headers(header_bytes) {
1520 let current = header_str
1521 .get_or_insert_with(|| String::from_utf8_lossy(header_bytes).into_owned());
1522 *current = secret.substitute_in_headers(current);
1523 }
1524 }
1525
1526 header_str.filter(|headers| headers.as_bytes() != header_bytes)
1527 }
1528
1529 fn consume_chunked_body_with_violation_detection(
1530 &mut self,
1531 state: &mut ChunkedBodyState,
1532 data: &[u8],
1533 ) -> Result<Option<usize>, SecretViolationAction> {
1534 let mut decoded_tail = std::mem::take(&mut state.decoded_tail);
1535 let body_end = process_chunked_body(state, data, |event| {
1536 match event {
1537 ChunkedBodyEvent::SizeLine(line) => {
1538 self.apply_chunked_metadata_policy(line, RequestLocation::ChunkMetadata)?;
1539 }
1540 ChunkedBodyEvent::Payload(payload) => {
1541 self.block_unsupported_body_placeholder(&decoded_tail, payload)?;
1542 self.apply_blocking_action(self.detect_http1_fragment_blocking_action(
1543 &decoded_tail,
1544 payload,
1545 "",
1546 RequestLocation::Body,
1547 ))?;
1548 update_tail_buffer(
1549 &mut decoded_tail,
1550 payload,
1551 self.max_detection_window_len.saturating_sub(1),
1552 );
1553 }
1554 ChunkedBodyEvent::ZeroChunk => {}
1555 ChunkedBodyEvent::TrailerLine(line) => {
1556 self.apply_chunked_metadata_policy(line, RequestLocation::Trailer)?;
1557 }
1558 }
1559 Ok(())
1560 });
1561 state.decoded_tail = decoded_tail;
1562 body_end
1563 }
1564
1565 fn rewrite_chunked_body_part(
1566 &mut self,
1567 state: &mut ChunkedRewriteState,
1568 data: &[u8],
1569 ) -> Result<ChunkedRewriteResult, SecretViolationAction> {
1570 let mut output = Vec::new();
1571 let mut decoded_tail = std::mem::take(&mut state.parser.decoded_tail);
1572 let mut substitution_tail = std::mem::take(&mut state.substitution_tail);
1573
1574 let body_end = process_chunked_body(&mut state.parser, data, |event| {
1575 match event {
1576 ChunkedBodyEvent::SizeLine(line) => {
1577 self.apply_chunked_metadata_policy(line, RequestLocation::ChunkMetadata)?;
1578 }
1579 ChunkedBodyEvent::Payload(payload) => {
1580 self.apply_blocking_action(self.detect_http1_fragment_blocking_action(
1581 &decoded_tail,
1582 payload,
1583 "",
1584 RequestLocation::Body,
1585 ))?;
1586 update_tail_buffer(
1587 &mut decoded_tail,
1588 payload,
1589 self.max_detection_window_len.saturating_sub(1),
1590 );
1591 self.append_rewritten_chunked_payload(
1592 &mut substitution_tail,
1593 payload,
1594 &mut output,
1595 );
1596 }
1597 ChunkedBodyEvent::ZeroChunk => {
1598 self.flush_rewritten_chunked_payload(&mut substitution_tail, &mut output);
1599 output.extend_from_slice(b"0\r\n");
1600 }
1601 ChunkedBodyEvent::TrailerLine(trailer_line) => {
1602 self.apply_chunked_metadata_policy(trailer_line, RequestLocation::Trailer)?;
1603 output.extend_from_slice(trailer_line);
1604 }
1605 }
1606 Ok(())
1607 })?;
1608
1609 state.parser.decoded_tail = decoded_tail;
1610 state.substitution_tail = substitution_tail;
1611
1612 Ok(ChunkedRewriteResult { output, body_end })
1613 }
1614
1615 fn append_rewritten_chunked_payload(
1616 &self,
1617 substitution_tail: &mut Vec<u8>,
1618 payload: &[u8],
1619 output: &mut Vec<u8>,
1620 ) {
1621 substitution_tail.extend_from_slice(payload);
1622 let carry_len = self.max_body_placeholder_len.saturating_sub(1);
1623 self.append_rewritten_chunked_prefix(substitution_tail, carry_len, output);
1624 }
1625
1626 fn flush_rewritten_chunked_payload(
1627 &self,
1628 substitution_tail: &mut Vec<u8>,
1629 output: &mut Vec<u8>,
1630 ) {
1631 self.append_rewritten_chunked_prefix(substitution_tail, 0, output);
1632 }
1633
1634 fn append_rewritten_chunked_prefix(
1635 &self,
1636 substitution_tail: &mut Vec<u8>,
1637 keep_len: usize,
1638 output: &mut Vec<u8>,
1639 ) {
1640 let safe_len = substitution_tail.len().saturating_sub(keep_len);
1641 if safe_len == 0 {
1642 return;
1643 }
1644
1645 let mut cursor = 0;
1646 let mut chunk_payload = Vec::with_capacity(safe_len);
1647 while cursor < safe_len {
1648 if let Some(secret) = self.matching_body_secret_at(&substitution_tail[cursor..]) {
1649 chunk_payload.extend_from_slice(secret.value.as_bytes());
1650 cursor += secret.placeholder.len();
1651 } else {
1652 chunk_payload.push(substitution_tail[cursor]);
1653 cursor += 1;
1654 }
1655 }
1656
1657 let kept = substitution_tail.split_off(cursor);
1658 *substitution_tail = kept;
1659 append_chunk(output, &chunk_payload);
1660 }
1661
1662 fn matching_body_secret_at(&self, data: &[u8]) -> Option<&EligibleSecret> {
1663 self.eligible_for_substitution.iter().find(|secret| {
1664 secret.substitute_body
1665 && !secret.placeholder.is_empty()
1666 && (!secret.require_tls_identity || self.tls_intercepted)
1667 && data.starts_with(secret.placeholder.as_bytes())
1668 })
1669 }
1670
1671 fn apply_blocking_action(
1672 &mut self,
1673 report: Option<SecretViolationReport>,
1674 ) -> Result<(), SecretViolationAction> {
1675 let Some(report) = report else {
1676 return Ok(());
1677 };
1678
1679 let action = report.action;
1680 self.http1_violation = matches!(report.protocol, RequestProtocol::Http1)
1681 && matches!(report.method.as_deref(), Some(method) if method != "HEAD" && method != "CONNECT");
1682
1683 self.log_violation(&report);
1684 Err(action.into_violation_action())
1685 }
1686
1687 fn log_violation(&self, report: &SecretViolationReport) {
1688 if matches!(report.action, BlockingAction::Block) {
1689 return;
1690 }
1691
1692 let host = report.host.as_deref().unwrap_or("");
1693 let method = report.method.as_deref().unwrap_or("");
1694 let path = report.path.as_deref().unwrap_or("");
1695 let guest_dst = self
1696 .guest_dst
1697 .map(|dst| dst.to_string())
1698 .unwrap_or_default();
1699 let http2_stream_id = report
1700 .http2_stream_id
1701 .map(|id| id.to_string())
1702 .unwrap_or_default();
1703
1704 match report.action {
1705 BlockingAction::Block => {}
1706 BlockingAction::BlockAndLog => tracing::warn!(
1707 action = %report.action,
1708 secret_env_var = %report.env_var,
1709 placeholder = %report.placeholder,
1710 protocol = %report.protocol,
1711 sni = %self.sni,
1712 host = %host,
1713 method = %method,
1714 path = %path,
1715 location = %report.location,
1716 match_form = %report.match_form,
1717 guest_dst = %guest_dst,
1718 http2_stream_id = %http2_stream_id,
1719 "secret violation: placeholder detected where substitution or passthrough is not permitted"
1720 ),
1721 BlockingAction::BlockAndTerminate => tracing::error!(
1722 action = %report.action,
1723 secret_env_var = %report.env_var,
1724 placeholder = %report.placeholder,
1725 protocol = %report.protocol,
1726 sni = %self.sni,
1727 host = %host,
1728 method = %method,
1729 path = %path,
1730 location = %report.location,
1731 match_form = %report.match_form,
1732 guest_dst = %guest_dst,
1733 http2_stream_id = %http2_stream_id,
1734 "secret violation: placeholder detected where substitution or passthrough is not permitted - terminating"
1735 ),
1736 }
1737 }
1738
1739 fn detect_blocking_action(
1747 &self,
1748 data: &[u8],
1749 headers: &str,
1750 location_hint: RequestLocation,
1751 ) -> Option<SecretViolationReport> {
1752 self.detect_http1_fragment_blocking_action(&self.prev_tail, data, headers, location_hint)
1753 }
1754
1755 fn detect_http1_fragment_blocking_action(
1758 &self,
1759 prev_tail: &[u8],
1760 data: &[u8],
1761 headers: &str,
1762 location_hint: RequestLocation,
1763 ) -> Option<SecretViolationReport> {
1764 let mut report = detect_blocking_action_with_tail(
1765 &self.ineligible_for_substitution,
1766 prev_tail,
1767 data,
1768 headers,
1769 RequestProtocol::Http1,
1770 location_hint,
1771 None,
1772 );
1773 if let Some(report) = &mut report
1774 && let Some(summary) = &self.http1_request_summary
1775 {
1776 report.apply_request_summary(summary);
1777 }
1778 report
1779 }
1780
1781 fn apply_chunked_metadata_policy(
1788 &mut self,
1789 line: &[u8],
1790 location: RequestLocation,
1791 ) -> Result<(), SecretViolationAction> {
1792 debug_assert!(matches!(
1793 location,
1794 RequestLocation::ChunkMetadata | RequestLocation::Trailer
1795 ));
1796 let headers = if location == RequestLocation::Trailer {
1797 std::str::from_utf8(line).unwrap_or_default()
1798 } else {
1799 ""
1800 };
1801 self.apply_blocking_action(self.detect_http1_fragment_blocking_action(
1802 &[],
1803 line,
1804 headers,
1805 location,
1806 ))
1807 }
1808
1809 fn update_tail(&mut self, data: &[u8]) {
1813 update_tail_buffer(
1814 &mut self.prev_tail,
1815 data,
1816 self.max_detection_window_len.saturating_sub(1),
1817 );
1818 }
1819}
1820
1821impl Http2State {
1822 fn process(
1823 &mut self,
1824 handler: &mut SecretsHandler,
1825 data: &[u8],
1826 ) -> Result<Vec<u8>, SecretViolationAction> {
1827 self.buffer.extend_from_slice(data);
1828 let mut output = Vec::new();
1829
1830 if !self.preface_seen {
1831 if self.buffer.len() < HTTP2_PREFACE.len() {
1832 return Ok(output);
1833 }
1834 if !self.buffer.starts_with(HTTP2_PREFACE) {
1835 return Err(SecretViolationAction::Block);
1836 }
1837 output.extend_from_slice(HTTP2_PREFACE);
1838 self.buffer.drain(..HTTP2_PREFACE.len());
1839 self.preface_seen = true;
1840 }
1841
1842 loop {
1843 if self.buffer.len() < 9 {
1844 break;
1845 }
1846
1847 let frame_len = http2_frame_payload_len(&self.buffer[..9]);
1848 if frame_len > MAX_HTTP2_FRAME_PAYLOAD_BYTES {
1849 return Err(SecretViolationAction::Block);
1850 }
1851 let full_len = 9 + frame_len;
1852 if self.buffer.len() < full_len {
1853 break;
1854 }
1855
1856 let frame = self.buffer[..full_len].to_vec();
1857 self.buffer.drain(..full_len);
1858 self.process_frame(handler, &frame, &mut output)?;
1859 }
1860
1861 Ok(output)
1862 }
1863
1864 fn process_frame(
1865 &mut self,
1866 handler: &mut SecretsHandler,
1867 raw: &[u8],
1868 output: &mut Vec<u8>,
1869 ) -> Result<(), SecretViolationAction> {
1870 let frame = parse_http2_frame(raw)?;
1871
1872 if self.header_block.is_some() && frame.kind != HTTP2_FRAME_CONTINUATION {
1873 return Err(SecretViolationAction::Block);
1874 }
1875
1876 match frame.kind {
1877 HTTP2_FRAME_HEADERS => self.process_headers_frame(handler, frame, output),
1878 HTTP2_FRAME_CONTINUATION => self.process_continuation_frame(handler, frame, output),
1879 HTTP2_FRAME_DATA => self.process_data_frame(handler, frame, output),
1880 HTTP2_FRAME_PUSH_PROMISE => Err(SecretViolationAction::Block),
1881 _ => {
1882 output.extend_from_slice(frame.raw);
1883 Ok(())
1884 }
1885 }
1886 }
1887
1888 fn process_headers_frame(
1889 &mut self,
1890 handler: &mut SecretsHandler,
1891 frame: Http2Frame<'_>,
1892 output: &mut Vec<u8>,
1893 ) -> Result<(), SecretViolationAction> {
1894 if frame.stream_id == 0 || frame.stream_id.is_multiple_of(2) || self.header_block.is_some()
1895 {
1896 return Err(SecretViolationAction::Block);
1897 }
1898
1899 let fragment = http2_headers_fragment(frame.flags, frame.payload)?;
1900 if fragment.len() > MAX_HTTP2_HEADER_BLOCK_BYTES {
1901 return Err(SecretViolationAction::Block);
1902 }
1903
1904 let block = Http2HeaderBlock {
1905 stream_id: frame.stream_id,
1906 end_stream: frame.flags & HTTP2_FLAG_END_STREAM != 0,
1907 block: fragment.to_vec(),
1908 };
1909
1910 if frame.flags & HTTP2_FLAG_END_HEADERS != 0 {
1911 self.finish_header_block(handler, block, output)
1912 } else {
1913 self.header_block = Some(block);
1914 Ok(())
1915 }
1916 }
1917
1918 fn process_continuation_frame(
1919 &mut self,
1920 handler: &mut SecretsHandler,
1921 frame: Http2Frame<'_>,
1922 output: &mut Vec<u8>,
1923 ) -> Result<(), SecretViolationAction> {
1924 let Some(mut block) = self.header_block.take() else {
1925 return Err(SecretViolationAction::Block);
1926 };
1927 if frame.stream_id == 0 || frame.stream_id != block.stream_id {
1928 return Err(SecretViolationAction::Block);
1929 }
1930
1931 block.block.extend_from_slice(frame.payload);
1932 if block.block.len() > MAX_HTTP2_HEADER_BLOCK_BYTES {
1933 return Err(SecretViolationAction::Block);
1934 }
1935
1936 if frame.flags & HTTP2_FLAG_END_HEADERS != 0 {
1937 self.finish_header_block(handler, block, output)
1938 } else {
1939 self.header_block = Some(block);
1940 Ok(())
1941 }
1942 }
1943
1944 fn process_data_frame(
1945 &mut self,
1946 handler: &mut SecretsHandler,
1947 frame: Http2Frame<'_>,
1948 output: &mut Vec<u8>,
1949 ) -> Result<(), SecretViolationAction> {
1950 if frame.stream_id == 0 || !self.open_request_streams.contains(&frame.stream_id) {
1951 return Err(SecretViolationAction::Block);
1952 }
1953
1954 let data = http2_data_payload(frame.flags, frame.payload)?;
1955 let tail = self.data_tails.entry(frame.stream_id).or_default();
1956 if handler.contains_eligible_body_placeholder(tail, data) {
1957 tracing::warn!(
1958 "secret substitution in HTTP/2 DATA frames is unsupported; blocking placeholder"
1959 );
1960 return Err(SecretViolationAction::Block);
1961 }
1962 let mut report = detect_blocking_action_with_tail(
1963 &handler.ineligible_for_substitution,
1964 tail,
1965 data,
1966 "",
1967 RequestProtocol::Http2,
1968 RequestLocation::Body,
1969 Some(frame.stream_id),
1970 );
1971 if let Some(report) = &mut report
1972 && let Some(summary) = self.request_summaries.get(&frame.stream_id)
1973 {
1974 report.apply_request_summary(summary);
1975 }
1976 handler.apply_blocking_action(report)?;
1977 update_tail_buffer(
1978 tail,
1979 data,
1980 handler.max_detection_window_len.saturating_sub(1),
1981 );
1982 if frame.flags & HTTP2_FLAG_END_STREAM != 0 {
1983 self.data_tails.remove(&frame.stream_id);
1984 self.open_request_streams.remove(&frame.stream_id);
1985 self.request_summaries.remove(&frame.stream_id);
1986 }
1987 output.extend_from_slice(frame.raw);
1988 Ok(())
1989 }
1990
1991 fn finish_header_block(
1992 &mut self,
1993 handler: &mut SecretsHandler,
1994 block: Http2HeaderBlock,
1995 output: &mut Vec<u8>,
1996 ) -> Result<(), SecretViolationAction> {
1997 let mut headers = self.decode_headers(&block.block)?;
1998 let is_initial_request = !self.open_request_streams.contains(&block.stream_id);
1999 if is_initial_request {
2000 if block.stream_id <= self.highest_client_stream_id {
2004 return Err(SecretViolationAction::Block);
2005 }
2006 if self.open_request_streams.len() >= MAX_HTTP2_TRACKED_STREAMS {
2007 return Err(SecretViolationAction::Block);
2008 }
2009 self.highest_client_stream_id = block.stream_id;
2010 self.open_request_streams.insert(block.stream_id);
2011 } else if !block.end_stream {
2012 return Err(SecretViolationAction::Block);
2013 }
2014
2015 if let Some(validator) = handler.http_authority.as_ref() {
2016 validate_http2_authority(&headers, validator, is_initial_request)?;
2017 }
2018
2019 let detection_bytes = http2_header_detection_bytes(&headers);
2020 let detection_text = String::from_utf8_lossy(&detection_bytes);
2021 let request_summary = http2_request_summary(detection_text.as_ref());
2022 if is_initial_request {
2023 handler.apply_blocking_action(detect_http2_header_blocking_action(
2024 &handler.ineligible_for_substitution,
2025 &headers,
2026 &request_summary,
2027 block.stream_id,
2028 ))?;
2029 handler.substitute_http2_headers(&mut headers);
2030 } else {
2031 let summary = self
2033 .request_summaries
2034 .get(&block.stream_id)
2035 .unwrap_or(&request_summary);
2036
2037 handler.apply_blocking_action(detect_blocking_action_in_fragments(
2038 &handler.ineligible_for_substitution,
2039 &[(&detection_bytes, RequestLocation::Trailer)],
2040 RequestProtocol::Http2,
2041 summary,
2042 Some(block.stream_id),
2043 ))?;
2044 }
2045
2046 let encoded = self.encode_headers(&headers)?;
2047 append_http2_header_frames(output, block.stream_id, block.end_stream, &encoded)?;
2048 if block.end_stream {
2049 self.data_tails.remove(&block.stream_id);
2050 self.open_request_streams.remove(&block.stream_id);
2051 self.request_summaries.remove(&block.stream_id);
2052 } else {
2053 self.request_summaries
2054 .insert(block.stream_id, request_summary);
2055 }
2056 Ok(())
2057 }
2058
2059 fn decode_headers(&mut self, block: &[u8]) -> Result<Http2Headers, SecretViolationAction> {
2060 check_header_block(block).map_err(|err| {
2063 tracing::debug!(error = %err, "rejecting guest HTTP/2 header block");
2064 SecretViolationAction::Block
2065 })?;
2066 let mut block = block.to_vec();
2067 let mut headers = Vec::new();
2068 let mut decoded_bytes = 0usize;
2069
2070 while !block.is_empty() {
2071 let before_len = block.len();
2072 let mut decoded = Vec::with_capacity(1);
2073 self.decoder
2074 .decode_exact(&mut block, &mut decoded)
2075 .map_err(|_| SecretViolationAction::Block)?;
2076 if decoded.is_empty() {
2077 if block.len() == before_len {
2078 return Err(SecretViolationAction::Block);
2079 }
2080 continue;
2081 }
2082
2083 if headers.len() >= MAX_HTTP2_HEADER_FIELDS {
2084 return Err(SecretViolationAction::Block);
2085 }
2086 let (name, value, _flags) = decoded.pop().expect("decoded one header");
2087 decoded_bytes = decoded_bytes
2088 .checked_add(name.len())
2089 .and_then(|len| len.checked_add(value.len()))
2090 .and_then(|len| len.checked_add(4))
2091 .ok_or(SecretViolationAction::Block)?;
2092 if decoded_bytes > MAX_HTTP2_DECODED_HEADER_BYTES {
2093 return Err(SecretViolationAction::Block);
2094 }
2095
2096 headers.push((name, value));
2097 }
2098
2099 Ok(headers)
2100 }
2101
2102 fn encode_headers(
2103 &mut self,
2104 headers: &[(Vec<u8>, Vec<u8>)],
2105 ) -> Result<Vec<u8>, SecretViolationAction> {
2106 let mut encoded = Vec::new();
2107 for (name, value) in headers {
2108 self.encoder
2109 .encode(
2110 (name.clone(), value.clone(), HpackEncoder::NEVER_INDEXED),
2111 &mut encoded,
2112 )
2113 .map_err(|_| SecretViolationAction::Block)?;
2114 }
2115 Ok(encoded)
2116 }
2117}
2118
2119fn header_field_allowed(headers_enabled: bool, header_fields: &[String], name: &[u8]) -> bool {
2128 headers_enabled
2129 && (header_fields.is_empty()
2130 || header_fields
2131 .iter()
2132 .any(|field| name.eq_ignore_ascii_case(field.as_bytes())))
2133}
2134
2135fn header_field_name(line: &[u8]) -> &[u8] {
2137 match line.iter().position(|byte| *byte == b':') {
2138 Some(end) => &line[..end],
2139 None => line,
2140 }
2141}
2142
2143fn is_authorization_header(line: &str) -> bool {
2146 line.as_bytes()
2147 .get(..AUTHORIZATION_HEADER_NAME.len())
2148 .is_some_and(|bytes| bytes.eq_ignore_ascii_case(AUTHORIZATION_HEADER_NAME))
2149}
2150
2151fn is_http2_preface_prefix(data: &[u8]) -> bool {
2152 !data.is_empty()
2153 && if data.len() <= HTTP2_PREFACE.len() {
2154 HTTP2_PREFACE.starts_with(data)
2155 } else {
2156 data.starts_with(HTTP2_PREFACE)
2157 }
2158}
2159
2160fn has_complete_http2_preface(data: &[u8]) -> bool {
2161 data.len() >= HTTP2_PREFACE.len() && data.starts_with(HTTP2_PREFACE)
2162}
2163
2164fn http2_frame_payload_len(header: &[u8]) -> usize {
2165 ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize
2166}
2167
2168fn parse_http2_frame(raw: &[u8]) -> Result<Http2Frame<'_>, SecretViolationAction> {
2169 if raw.len() < 9 {
2170 return Err(SecretViolationAction::Block);
2171 }
2172 let len = http2_frame_payload_len(raw);
2173 if raw.len() != 9 + len {
2174 return Err(SecretViolationAction::Block);
2175 }
2176
2177 let stream_id = u32::from_be_bytes([raw[5], raw[6], raw[7], raw[8]]) & 0x7fff_ffff;
2178 Ok(Http2Frame {
2179 kind: raw[3],
2180 flags: raw[4],
2181 stream_id,
2182 payload: &raw[9..],
2183 raw,
2184 })
2185}
2186
2187fn http2_headers_fragment(flags: u8, payload: &[u8]) -> Result<&[u8], SecretViolationAction> {
2188 let mut start = 0;
2189 let pad_len = if flags & HTTP2_FLAG_PADDED != 0 {
2190 let Some(pad_len) = payload.first() else {
2191 return Err(SecretViolationAction::Block);
2192 };
2193 start = 1;
2194 *pad_len as usize
2195 } else {
2196 0
2197 };
2198
2199 if flags & HTTP2_FLAG_PRIORITY != 0 {
2200 start += 5;
2201 }
2202 if payload.len() < start + pad_len {
2203 return Err(SecretViolationAction::Block);
2204 }
2205
2206 Ok(&payload[start..payload.len() - pad_len])
2207}
2208
2209fn http2_data_payload(flags: u8, payload: &[u8]) -> Result<&[u8], SecretViolationAction> {
2210 if flags & HTTP2_FLAG_PADDED == 0 {
2211 return Ok(payload);
2212 }
2213
2214 let Some(pad_len) = payload.first() else {
2215 return Err(SecretViolationAction::Block);
2216 };
2217 let pad_len = *pad_len as usize;
2218 if payload.len() < 1 + pad_len {
2219 return Err(SecretViolationAction::Block);
2220 }
2221
2222 Ok(&payload[1..payload.len() - pad_len])
2223}
2224
2225fn append_http2_header_frames(
2226 output: &mut Vec<u8>,
2227 stream_id: u32,
2228 end_stream: bool,
2229 block: &[u8],
2230) -> Result<(), SecretViolationAction> {
2231 let mut first = true;
2232 let mut offset = 0;
2233
2234 while first || offset < block.len() {
2235 let remaining = block.len().saturating_sub(offset);
2236 let take = remaining.min(HTTP2_OUTBOUND_FRAME_PAYLOAD_BYTES);
2237 let payload = &block[offset..offset + take];
2238 offset += take;
2239
2240 let kind = if first {
2241 HTTP2_FRAME_HEADERS
2242 } else {
2243 HTTP2_FRAME_CONTINUATION
2244 };
2245 let mut flags = 0;
2246 if offset == block.len() {
2247 flags |= HTTP2_FLAG_END_HEADERS;
2248 }
2249 if first && end_stream {
2250 flags |= HTTP2_FLAG_END_STREAM;
2251 }
2252
2253 append_http2_frame(output, kind, flags, stream_id, payload)?;
2254 first = false;
2255 }
2256
2257 Ok(())
2258}
2259
2260fn append_http2_frame(
2261 output: &mut Vec<u8>,
2262 kind: u8,
2263 flags: u8,
2264 stream_id: u32,
2265 payload: &[u8],
2266) -> Result<(), SecretViolationAction> {
2267 if payload.len() > 0x00ff_ffff || stream_id & 0x8000_0000 != 0 {
2268 return Err(SecretViolationAction::Block);
2269 }
2270
2271 output.push(((payload.len() >> 16) & 0xff) as u8);
2272 output.push(((payload.len() >> 8) & 0xff) as u8);
2273 output.push((payload.len() & 0xff) as u8);
2274 output.push(kind);
2275 output.push(flags);
2276 output.extend_from_slice(&stream_id.to_be_bytes());
2277 output.extend_from_slice(payload);
2278 Ok(())
2279}
2280
2281fn validate_http1_authority(
2282 metadata: &HttpRequestMetadata,
2283 validator: &HttpAuthorityValidator,
2284) -> Result<(), SecretViolationAction> {
2285 if metadata.host_headers.len() != 1 {
2286 return Err(SecretViolationAction::Block);
2287 }
2288
2289 for authority in metadata
2290 .host_headers
2291 .iter()
2292 .chain(metadata.target_authority.iter())
2293 {
2294 validate_authority(authority, validator)?;
2295 }
2296
2297 Ok(())
2298}
2299
2300fn validate_http2_authority(
2301 headers: &[(Vec<u8>, Vec<u8>)],
2302 validator: &HttpAuthorityValidator,
2303 require_authority: bool,
2304) -> Result<(), SecretViolationAction> {
2305 let mut authority_count = 0usize;
2306
2307 for (name, value) in headers {
2308 if name.eq_ignore_ascii_case(b":authority") {
2309 authority_count += 1;
2310 let authority = String::from_utf8_lossy(value);
2311 validate_authority(authority.as_ref(), validator)?;
2312 } else if name.eq_ignore_ascii_case(b"host") {
2313 let host = String::from_utf8_lossy(value);
2314 validate_authority(host.as_ref(), validator)?;
2315 }
2316 }
2317
2318 if require_authority && authority_count != 1 {
2319 return Err(SecretViolationAction::Block);
2320 }
2321
2322 Ok(())
2323}
2324
2325fn validate_authority(
2326 authority: &str,
2327 validator: &HttpAuthorityValidator,
2328) -> Result<(), SecretViolationAction> {
2329 match validator {
2330 HttpAuthorityValidator::Sni(sni) => authority_matches_sni(authority, sni)
2331 .then_some(())
2332 .ok_or(SecretViolationAction::Block),
2333 HttpAuthorityValidator::Policy {
2334 guest_dst,
2335 network_policy,
2336 shared,
2337 secret_host,
2338 } => {
2339 if secret_host
2342 .as_ref()
2343 .is_some_and(|host| !authority_matches_sni(authority, host))
2344 {
2345 return Err(SecretViolationAction::Block);
2346 }
2347 let Some(hostname) = authority_hostname(authority) else {
2348 return Err(SecretViolationAction::Block);
2349 };
2350 let hostname = hostname.to_ascii_lowercase();
2351 let authority_dst = SocketAddr::new(guest_dst.ip(), guest_dst.port());
2352 match network_policy.evaluate_egress_with_source(
2353 authority_dst,
2354 Protocol::Tcp,
2355 shared,
2356 HostnameSource::Sni(&hostname),
2357 ) {
2358 EgressEvaluation::Allow => Ok(()),
2359 EgressEvaluation::Deny | EgressEvaluation::DeferUntilHostname => {
2360 Err(SecretViolationAction::Block)
2361 }
2362 }
2363 }
2364 }
2365}
2366
2367fn http2_header_detection_bytes(headers: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
2368 let len = headers
2369 .iter()
2370 .map(|(name, value)| name.len() + value.len() + 4)
2371 .sum();
2372 let mut out = Vec::with_capacity(len);
2373 for (name, value) in headers {
2374 out.extend_from_slice(name);
2375 out.extend_from_slice(b": ");
2376 out.extend_from_slice(value);
2377 out.extend_from_slice(b"\r\n");
2378 }
2379 out
2380}
2381
2382fn parse_http_request_metadata(
2383 header_bytes: &[u8],
2384) -> Result<Option<HttpRequestMetadata>, SecretViolationAction> {
2385 let headers = std::str::from_utf8(header_bytes).map_err(|_| SecretViolationAction::Block)?;
2386 let mut lines = headers.split("\r\n").skip_while(|line| line.is_empty());
2387 let Some(request_line) = lines.next() else {
2388 return Ok(None);
2389 };
2390
2391 let Some((method, target, version)) = split_http_request_line(request_line) else {
2392 return Err(SecretViolationAction::Block);
2393 };
2394 if version == "HTTP/2.0" {
2395 return Err(SecretViolationAction::Block);
2396 }
2397 if !version.starts_with("HTTP/1.") {
2398 return Err(SecretViolationAction::Block);
2399 }
2400
2401 let target_authority = request_target_authority(method, target)?;
2402 let mut host_headers = Vec::new();
2403 for line in lines.take_while(|line| !line.is_empty()) {
2404 let Some((name, value)) = line.split_once(':') else {
2405 return Err(SecretViolationAction::Block);
2406 };
2407 if name.is_empty() || !name.bytes().all(is_http_token_byte) {
2408 return Err(SecretViolationAction::Block);
2409 }
2410 let value = value.trim();
2411
2412 if name.eq_ignore_ascii_case("host") {
2413 host_headers.push(value.to_string());
2414 }
2415 }
2416
2417 if host_headers.is_empty() {
2418 return Err(SecretViolationAction::Block);
2419 }
2420
2421 Ok(Some(HttpRequestMetadata {
2422 host_headers,
2423 target_authority,
2424 }))
2425}
2426
2427fn http_request_version(request_line: &str) -> Option<&str> {
2428 split_http_request_line(request_line).map(|(_, _, version)| version)
2429}
2430
2431fn split_http_request_line(request_line: &str) -> Option<(&str, &str, &str)> {
2432 let mut parts = request_line.split_whitespace();
2433 let method = parts.next()?;
2434 let target = parts.next()?;
2435 let version = parts.next()?;
2436 if parts.next().is_some() || !method.bytes().all(is_http_token_byte) {
2437 return None;
2438 }
2439 Some((method, target, version))
2440}
2441
2442fn request_target_authority(
2443 method: &str,
2444 target: &str,
2445) -> Result<Option<String>, SecretViolationAction> {
2446 if target.starts_with('/') || target == "*" {
2447 return Ok(None);
2448 }
2449
2450 if let Some(authority) = absolute_form_authority(target)? {
2451 return Ok(Some(authority.to_string()));
2452 }
2453
2454 if method.eq_ignore_ascii_case("CONNECT") {
2455 if target.is_empty() || target.contains('/') || target.contains('@') {
2456 return Err(SecretViolationAction::Block);
2457 }
2458 return Ok(Some(target.to_string()));
2459 }
2460
2461 Err(SecretViolationAction::Block)
2462}
2463
2464fn absolute_form_authority(target: &str) -> Result<Option<&str>, SecretViolationAction> {
2465 let Some((scheme, rest)) = target.split_once("://") else {
2466 return Ok(None);
2467 };
2468 if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {
2469 return Err(SecretViolationAction::Block);
2470 }
2471
2472 let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
2473 let authority = &rest[..authority_end];
2474 if authority.is_empty() || authority.contains('@') {
2475 return Err(SecretViolationAction::Block);
2476 }
2477 Ok(Some(authority))
2478}
2479
2480fn redacted_request_path(target: &str) -> String {
2481 let without_query = target.split_once('?').map_or(target, |(path, _)| path);
2482 if let Some(scheme_end) = without_query.find("://") {
2483 let after_scheme = &without_query[scheme_end + 3..];
2484 if let Some(path_start) = after_scheme.find('/') {
2485 return after_scheme[path_start..].to_string();
2486 }
2487 return "/".to_string();
2488 }
2489 without_query.to_string()
2490}
2491
2492fn request_summary(headers: &str, protocol: RequestProtocol) -> RequestSummary {
2493 match protocol {
2494 RequestProtocol::Http1 => http1_request_summary(headers),
2495 RequestProtocol::Http2 => http2_request_summary(headers),
2496 RequestProtocol::Opaque => RequestSummary::default(),
2497 }
2498}
2499
2500fn http1_request_summary(headers: &str) -> RequestSummary {
2501 let mut lines = headers.split("\r\n").skip_while(|line| line.is_empty());
2502 let Some(request_line) = lines.next() else {
2503 return RequestSummary::default();
2504 };
2505 let Some((method, target, _version)) = split_http_request_line(request_line) else {
2506 return RequestSummary::default();
2507 };
2508
2509 let host = lines
2510 .take_while(|line| !line.is_empty())
2511 .filter_map(|line| line.split_once(':'))
2512 .find_map(|(name, value)| name.eq_ignore_ascii_case("host").then(|| value.trim()));
2513
2514 RequestSummary {
2515 method: Some(method.to_string()),
2516 path: Some(redacted_request_path(target)),
2517 host: host.map(ToOwned::to_owned),
2518 }
2519}
2520
2521fn http2_request_summary(headers: &str) -> RequestSummary {
2522 let mut summary = RequestSummary::default();
2523 for line in headers.split("\r\n").filter(|line| !line.is_empty()) {
2524 if let Some(value) = line.strip_prefix(":method: ") {
2525 summary.method = Some(value.to_string());
2526 } else if let Some(value) = line.strip_prefix(":path: ") {
2527 summary.path = Some(redacted_request_path(value));
2528 } else if let Some(value) = line.strip_prefix(":authority: ") {
2529 summary.host = Some(value.trim().to_string());
2530 }
2531 }
2532 summary
2533}
2534
2535pub(crate) fn looks_like_http_request_prefix(data: &[u8]) -> bool {
2536 if data.is_empty() || b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".starts_with(data) {
2537 return true;
2538 }
2539
2540 let data = skip_leading_empty_http_lines(data);
2541 if data.is_empty() {
2542 return true;
2543 }
2544
2545 if http_request_line_has_binary_control(data) {
2546 return false;
2547 }
2548
2549 let method_end = data.iter().position(|byte| matches!(byte, b' ' | b'\t'));
2550 let method = match method_end {
2551 Some(end) => &data[..end],
2552 None => data,
2553 };
2554
2555 if method.is_empty() || !method.iter().copied().all(is_http_token_byte) {
2556 return false;
2557 }
2558
2559 let Some(tab) = method_end.filter(|end| data[*end] == b'\t') else {
2560 return true;
2561 };
2562 let target = &data[tab + 1..];
2563 let target = &target[..target
2564 .iter()
2565 .position(u8::is_ascii_whitespace)
2566 .unwrap_or(target.len())];
2567 target.is_empty()
2568 || target.starts_with(b"/")
2569 || target.starts_with(b"*")
2570 || method.eq_ignore_ascii_case(b"CONNECT")
2571 || [b"http://".as_slice(), b"https://".as_slice()]
2572 .iter()
2573 .any(|scheme| {
2574 let overlap = target.len().min(scheme.len());
2575 target[..overlap].eq_ignore_ascii_case(&scheme[..overlap])
2576 })
2577}
2578
2579fn http_request_line_has_binary_control(data: &[u8]) -> bool {
2580 let data = skip_leading_empty_http_lines(data);
2581 let request_line_end = data
2582 .iter()
2583 .position(|byte| matches!(byte, b'\r' | b'\n'))
2584 .unwrap_or(data.len());
2585 data[..request_line_end]
2586 .iter()
2587 .any(|byte| byte.is_ascii_control() && !byte.is_ascii_whitespace())
2588}
2589
2590fn opaque_prefix_might_be_http(data: &[u8]) -> bool {
2591 let data = skip_leading_empty_http_lines(data);
2592 let line_end = data
2593 .iter()
2594 .position(|byte| matches!(byte, b'\r' | b'\n'))
2595 .unwrap_or(data.len());
2596 let line = &data[..line_end];
2597 let delimiter = line
2598 .iter()
2599 .position(|byte| *byte == b' ' || (!byte.is_ascii_whitespace() && byte.is_ascii_control()))
2600 .unwrap_or(line.len());
2601 let method = &line[..delimiter];
2602 let has_binary_control = line
2603 .iter()
2604 .any(|byte| byte.is_ascii_control() && !byte.is_ascii_whitespace());
2605
2606 (has_binary_control
2607 && !method.is_empty()
2608 && [
2609 b"CONNECT".as_slice(),
2610 b"DELETE".as_slice(),
2611 b"GET".as_slice(),
2612 b"HEAD".as_slice(),
2613 b"OPTIONS".as_slice(),
2614 b"PATCH".as_slice(),
2615 b"POST".as_slice(),
2616 b"PUT".as_slice(),
2617 b"TRACE".as_slice(),
2618 ]
2619 .contains(&method))
2620 || line
2621 .windows(5)
2622 .any(|window| window.eq_ignore_ascii_case(b"HTTP/"))
2623}
2624
2625pub(crate) fn first_line_is_not_http_request(data: &[u8]) -> bool {
2626 let data = skip_leading_empty_http_lines(data);
2627 let Some(line_end) = data.windows(2).position(|window| window == b"\r\n") else {
2628 return false;
2629 };
2630 let line = String::from_utf8_lossy(&data[..line_end]);
2631 http_request_version(line.as_ref()).is_none()
2632}
2633
2634pub(crate) fn skip_leading_empty_http_lines(mut data: &[u8]) -> &[u8] {
2635 while data.starts_with(b"\r\n") {
2636 data = &data[2..];
2637 }
2638 data
2639}
2640
2641fn is_http_token_byte(byte: u8) -> bool {
2642 matches!(
2643 byte,
2644 b'!' | b'#'
2645 | b'$'
2646 | b'%'
2647 | b'&'
2648 | b'\''
2649 | b'*'
2650 | b'+'
2651 | b'-'
2652 | b'.'
2653 | b'^'
2654 | b'_'
2655 | b'`'
2656 | b'|'
2657 | b'~'
2658 | b'0'..=b'9'
2659 | b'A'..=b'Z'
2660 | b'a'..=b'z'
2661 )
2662}
2663
2664fn authority_matches_sni(authority: &str, sni: &str) -> bool {
2665 authority_hostname(authority).is_some_and(|hostname| {
2666 if let Ok(address) = sni.parse::<IpAddr>() {
2667 return hostname.parse::<IpAddr>().is_ok_and(|host| host == address);
2668 }
2669 hostname.eq_ignore_ascii_case(sni.trim_end_matches('.'))
2670 })
2671}
2672
2673fn authority_hostname(authority: &str) -> Option<&str> {
2674 let authority = authority.trim().trim_end_matches('.');
2675 if authority.is_empty() {
2676 return None;
2677 }
2678
2679 if let Some(rest) = authority.strip_prefix('[') {
2680 let (host, _port) = rest.split_once(']')?;
2681 return Some(host.trim_end_matches('.'));
2682 }
2683
2684 match authority.rsplit_once(':') {
2685 Some((host, port)) if !host.contains(':') && port.parse::<u16>().is_ok() => {
2686 Some(host.trim_end_matches('.'))
2687 }
2688 _ => Some(authority),
2689 }
2690}
2691
2692fn secret_host_allowed(
2693 secret: &SecretEntry,
2694 sni: &str,
2695 identity: Option<&SecretHostIdentity<'_>>,
2696) -> bool {
2697 secret
2698 .allowed_hosts
2699 .iter()
2700 .any(|pattern| host_pattern_allowed(pattern, sni, identity))
2701}
2702
2703fn host_pattern_allowed(
2704 pattern: &HostPattern,
2705 sni: &str,
2706 identity: Option<&SecretHostIdentity<'_>>,
2707) -> bool {
2708 if !pattern.matches(sni) {
2709 return false;
2710 }
2711 if matches!(pattern, HostPattern::Any) {
2712 return true;
2713 }
2714 let Some(identity) = identity else {
2715 return true;
2716 };
2717
2718 host_alias_matches(pattern, sni, identity)
2719 || identity
2720 .shared
2721 .any_resolved_hostname(identity.guest_ip, |hostname| pattern.matches(hostname))
2722}
2723
2724fn host_alias_matches(pattern: &HostPattern, sni: &str, identity: &SecretHostIdentity<'_>) -> bool {
2725 if !sni.eq_ignore_ascii_case(crate::HOST_ALIAS) || !pattern.matches(crate::HOST_ALIAS) {
2726 return false;
2727 }
2728
2729 identity
2730 .shared
2731 .gateway_ipv4()
2732 .is_some_and(|ip| identity.guest_ip == IpAddr::V4(ip))
2733 || identity
2734 .shared
2735 .gateway_ipv6()
2736 .is_some_and(|ip| identity.guest_ip == IpAddr::V6(ip))
2737}
2738
2739fn decode_basic_credentials(line: &str) -> Option<String> {
2743 let (_, raw_value) = line.split_once(':')?;
2744 let (scheme, encoded) = split_auth_scheme(raw_value.trim_start())?;
2745 if !scheme.eq_ignore_ascii_case("basic") {
2746 return None;
2747 }
2748 let bytes = BASE64.decode(encoded.trim()).ok()?;
2749 String::from_utf8(bytes).ok()
2750}
2751
2752fn opaque_basic_auth_payload(line: &[u8]) -> Option<&[u8]> {
2753 let value = line
2754 .get(AUTHORIZATION_HEADER_NAME.len()..)?
2755 .trim_ascii_start();
2756 let scheme_end = value.iter().position(|byte| byte.is_ascii_whitespace())?;
2757 let (scheme, encoded) = value.split_at(scheme_end);
2758 if !scheme.eq_ignore_ascii_case(b"basic") {
2759 return None;
2760 }
2761 let encoded = encoded.trim_ascii_start();
2762 (!encoded.is_empty()
2763 && encoded
2764 .iter()
2765 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'=')))
2766 .then_some(encoded)
2767}
2768
2769fn split_auth_scheme(header_value: &str) -> Option<(&str, &str)> {
2772 let split_at = header_value.find(char::is_whitespace)?;
2773 let (scheme, rest) = header_value.split_at(split_at);
2774 Some((scheme, rest.trim_start()))
2775}
2776
2777fn substitute_query_in_request_line(line: &str, placeholder: &str, value: &str) -> Option<String> {
2778 if placeholder.is_empty() {
2779 return None;
2780 }
2781
2782 let method_end = line.find(' ')?;
2783 let target_start = method_end + 1;
2784 let version_start = line[target_start..].rfind(' ')? + target_start;
2785 if version_start <= target_start {
2786 return None;
2787 }
2788
2789 let target = &line[target_start..version_start];
2790 let query_start = target.find('?')? + 1;
2791 let query = &target[query_start..];
2792 if !query.contains(placeholder) {
2793 return None;
2794 }
2795
2796 let mut result = String::with_capacity(line.len());
2797 result.push_str(&line[..target_start + query_start]);
2798 result.push_str(&query.replace(placeholder, value));
2799 result.push_str(&line[version_start..]);
2800 Some(result)
2801}
2802
2803fn substitute_query_in_target(target: &str, placeholder: &str, value: &str) -> Option<String> {
2804 if placeholder.is_empty() {
2805 return None;
2806 }
2807
2808 let query_start = target.find('?')? + 1;
2809 let query = &target[query_start..];
2810 if !query.contains(placeholder) {
2811 return None;
2812 }
2813
2814 let mut result = String::with_capacity(target.len());
2815 result.push_str(&target[..query_start]);
2816 result.push_str(&query.replace(placeholder, value));
2817 Some(result)
2818}
2819
2820fn substitute_basic_auth_value(
2821 header_value: &str,
2822 placeholder: &str,
2823 value: &str,
2824) -> Option<String> {
2825 let (scheme, encoded) = split_auth_scheme(header_value.trim_start())?;
2826 if !scheme.eq_ignore_ascii_case("basic") {
2827 return None;
2828 }
2829 let bytes = BASE64.decode(encoded.trim()).ok()?;
2830 let decoded = String::from_utf8(bytes).ok()?;
2831 if !decoded.contains(placeholder) {
2832 return None;
2833 }
2834 let replaced = decoded.replace(placeholder, value);
2835 Some(format!("Basic {}", BASE64.encode(replaced.as_bytes())))
2836}
2837
2838fn basic_auth_decoded_contains(headers: &str, placeholder: &str) -> bool {
2841 decoded_basic_auth_credentials(headers)
2842 .iter()
2843 .any(|decoded| decoded.contains(placeholder))
2844}
2845
2846fn decoded_basic_auth_credentials(headers: &str) -> Vec<String> {
2848 headers
2849 .split("\r\n")
2850 .filter(|line| is_authorization_header(line))
2851 .filter_map(decode_basic_credentials)
2852 .collect()
2853}
2854
2855fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
2857 if needle.is_empty() || haystack.len() < needle.len() {
2858 return false;
2859 }
2860 haystack.windows(needle.len()).any(|w| w == needle)
2861}
2862
2863fn max_placeholder_detection_len(placeholder_len: usize) -> usize {
2867 placeholder_len.saturating_mul(6)
2868}
2869
2870fn next_state_after_headers(
2876 headers: &str,
2877 body_bytes: &[u8],
2878) -> Result<RequestFraming, SecretViolationAction> {
2879 let body_in_chunk = body_bytes.len();
2880 let body_substitution_allowed = !has_non_identity_content_encoding(headers);
2881 let transfer_encoding = parse_transfer_encoding(headers)?;
2882 let content_length = parse_content_length(headers)?;
2883 if transfer_encoding.is_some() && content_length.is_some() {
2884 return Err(SecretViolationAction::Block);
2885 }
2886
2887 if transfer_encoding == Some(TransferEncoding::Chunked) {
2888 let mut chunked_state = ChunkedBodyState::default();
2889 let (state, body_in_request) = match consume_chunked_body(&mut chunked_state, body_bytes)? {
2890 Some(end) => (HttpState::AwaitingHeaders, end),
2891 _ => (
2892 HttpState::InChunkedBody {
2893 state: chunked_state,
2894 },
2895 body_in_chunk,
2896 ),
2897 };
2898 return Ok(RequestFraming {
2899 state,
2900 body_in_request,
2901 body_substitution_allowed: false,
2902 });
2903 }
2904 match content_length {
2905 Some(cl) if body_in_chunk >= cl => Ok(RequestFraming {
2906 state: HttpState::AwaitingHeaders,
2907 body_in_request: cl,
2908 body_substitution_allowed,
2909 }),
2910 Some(cl) => Ok(RequestFraming {
2911 state: HttpState::InBody {
2912 remaining: cl - body_in_chunk,
2913 },
2914 body_in_request: body_in_chunk,
2915 body_substitution_allowed,
2916 }),
2917 None => Ok(RequestFraming {
2921 state: HttpState::AwaitingHeaders,
2922 body_in_request: 0,
2923 body_substitution_allowed: false,
2924 }),
2925 }
2926}
2927
2928fn parse_content_length(headers: &str) -> Result<Option<usize>, SecretViolationAction> {
2931 let mut content_length = None;
2932 for line in headers.split("\r\n") {
2933 let Some((name, value)) = line.split_once(':') else {
2934 continue;
2935 };
2936 if name.eq_ignore_ascii_case("content-length") {
2937 let parsed = value
2938 .trim()
2939 .parse::<usize>()
2940 .map_err(|_| SecretViolationAction::Block)?;
2941 if content_length.is_some_and(|existing| existing != parsed) {
2942 return Err(SecretViolationAction::Block);
2943 }
2944 content_length = Some(parsed);
2945 }
2946 }
2947 Ok(content_length)
2948}
2949
2950fn content_length_exceeds_buffer_limit(headers: &str) -> Result<bool, SecretViolationAction> {
2951 Ok(parse_content_length(headers)?.is_some_and(|len| len > MAX_HTTP_BODY_BUFFER_BYTES))
2952}
2953
2954fn parse_transfer_encoding(
2956 headers: &str,
2957) -> Result<Option<TransferEncoding>, SecretViolationAction> {
2958 let mut saw_chunked = false;
2959 for line in headers.split("\r\n") {
2960 let Some((name, value)) = line.split_once(':') else {
2961 continue;
2962 };
2963 if !name.eq_ignore_ascii_case("transfer-encoding") {
2964 continue;
2965 }
2966
2967 for coding in value.split(',') {
2968 let coding = coding.trim();
2969 let coding_name = coding
2970 .split_once(';')
2971 .map_or(coding, |(name, _)| name)
2972 .trim();
2973 if coding_name.is_empty() || !coding_name.eq_ignore_ascii_case("chunked") {
2974 return Err(SecretViolationAction::Block);
2975 }
2976 if saw_chunked {
2977 return Err(SecretViolationAction::Block);
2978 }
2979 saw_chunked = true;
2980 }
2981 }
2982 Ok(saw_chunked.then_some(TransferEncoding::Chunked))
2983}
2984
2985fn has_non_identity_content_encoding(headers: &str) -> bool {
2987 for line in headers.split("\r\n") {
2988 let Some((name, value)) = line.split_once(':') else {
2989 continue;
2990 };
2991 if !name.eq_ignore_ascii_case("content-encoding") {
2992 continue;
2993 }
2994 if value
2995 .split(',')
2996 .any(|encoding| !encoding.trim().eq_ignore_ascii_case("identity"))
2997 {
2998 return true;
2999 }
3000 }
3001 false
3002}
3003
3004fn replace_bytes(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Option<Vec<u8>> {
3009 if !contains_bytes(haystack, needle) {
3010 return None;
3011 }
3012
3013 let mut result = Vec::with_capacity(haystack.len());
3014 let mut cursor = 0;
3015 while cursor < haystack.len() {
3016 if haystack[cursor..].starts_with(needle) {
3017 result.extend_from_slice(replacement);
3018 cursor += needle.len();
3019 } else {
3020 result.push(haystack[cursor]);
3021 cursor += 1;
3022 }
3023 }
3024 Some(result)
3025}
3026
3027#[cfg(test)]
3029fn url_decoded_contains(haystack: &[u8], needle: &[u8]) -> bool {
3030 let decoded: Vec<u8> = percent_decode(haystack).collect();
3031 contains_bytes(&decoded, needle)
3032}
3033
3034#[cfg(test)]
3038fn json_escaped_contains(haystack: &[u8], needle: &[u8]) -> bool {
3039 let decoded = json_unescape(haystack);
3040 contains_bytes(&decoded, needle)
3041}
3042
3043fn json_unescape(haystack: &[u8]) -> Vec<u8> {
3045 let mut decoded = Vec::with_capacity(haystack.len());
3046 let mut i = 0;
3047 while i < haystack.len() {
3048 if haystack[i] == b'\\'
3049 && i + 5 < haystack.len()
3050 && haystack[i + 1] == b'u'
3051 && let (Some(a), Some(b), Some(c), Some(d)) = (
3052 hex_digit(haystack[i + 2]),
3053 hex_digit(haystack[i + 3]),
3054 hex_digit(haystack[i + 4]),
3055 hex_digit(haystack[i + 5]),
3056 )
3057 {
3058 let cp = ((a as u32) << 12) | ((b as u32) << 8) | ((c as u32) << 4) | (d as u32);
3059 if let Some(ch) = char::from_u32(cp) {
3060 let mut buf = [0u8; 4];
3061 decoded.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
3062 }
3063 i += 6;
3064 continue;
3065 }
3066 decoded.push(haystack[i]);
3067 i += 1;
3068 }
3069 decoded
3070}
3071
3072fn hex_digit(b: u8) -> Option<u8> {
3073 (b as char).to_digit(16).map(|d| d as u8)
3074}
3075
3076fn update_content_length(headers: &str, new_len: usize) -> String {
3081 let mut result = String::with_capacity(headers.len());
3082 for (i, line) in headers.split("\r\n").enumerate() {
3083 if i > 0 {
3084 result.push_str("\r\n");
3085 }
3086 if line
3087 .as_bytes()
3088 .get(..15)
3089 .is_some_and(|b| b.eq_ignore_ascii_case(b"content-length:"))
3090 {
3091 result.push_str(&format!("Content-Length: {new_len}"));
3092 } else {
3093 result.push_str(line);
3094 }
3095 }
3096 result
3097}
3098
3099fn find_header_boundary(data: &[u8]) -> Option<usize> {
3101 data.windows(4)
3102 .position(|w| w == b"\r\n\r\n")
3103 .map(|pos| pos + 4)
3104}
3105
3106fn append_chunk(output: &mut Vec<u8>, payload: &[u8]) {
3107 if payload.is_empty() {
3108 return;
3109 }
3110 output.extend_from_slice(format!("{:X}\r\n", payload.len()).as_bytes());
3111 output.extend_from_slice(payload);
3112 output.extend_from_slice(b"\r\n");
3113}
3114
3115fn detect_blocking_action_with_tail(
3119 ineligible_for_substitution: &[IneligibleSecret],
3120 prev_tail: &[u8],
3121 data: &[u8],
3122 headers: &str,
3123 protocol: RequestProtocol,
3124 location_hint: RequestLocation,
3125 http2_stream_id: Option<u32>,
3126) -> Option<SecretViolationReport> {
3127 if ineligible_for_substitution.is_empty() {
3128 return None;
3129 }
3130
3131 let scan;
3132 let mut fragments = Vec::new();
3133 let summary = if matches!(protocol, RequestProtocol::Http1)
3134 && !headers.is_empty()
3135 && !is_scoped_fragment_location(location_hint)
3136 {
3137 let (request_line, metadata) = headers.split_once("\r\n").unwrap_or((headers, ""));
3138 if let Some((method, target, version)) = split_http_request_line(request_line) {
3139 fragments.push((method.as_bytes(), RequestLocation::Unknown));
3140 push_request_target_fragments(&mut fragments, target.as_bytes());
3141 fragments.push((version.as_bytes(), RequestLocation::Unknown));
3142 } else {
3143 fragments.push((request_line.as_bytes(), RequestLocation::Unknown));
3144 }
3145
3146 fragments.push((metadata.as_bytes(), RequestLocation::Header));
3147 let body_start = find_header_boundary(data).unwrap_or(data.len());
3149 fragments.push((&data[body_start..], RequestLocation::Body));
3150
3151 request_summary(headers, protocol)
3152 } else {
3153 scan = if prev_tail.is_empty() {
3154 Cow::Borrowed(data)
3155 } else {
3156 let mut stitched = Vec::with_capacity(prev_tail.len() + data.len());
3157 stitched.extend_from_slice(prev_tail);
3158 stitched.extend_from_slice(data);
3159 Cow::Owned(stitched)
3160 };
3161
3162 fragments.push((scan.as_ref(), location_hint));
3163 RequestSummary::default()
3164 };
3165
3166 detect_blocking_action_in_fragments(
3167 ineligible_for_substitution,
3168 &fragments,
3169 protocol,
3170 &summary,
3171 http2_stream_id,
3172 )
3173}
3174
3175fn push_request_target_fragments<'a>(
3178 fragments: &mut Vec<(&'a [u8], RequestLocation)>,
3179 target: &'a [u8],
3180) {
3181 if let Some(query_start) = target.iter().position(|byte| *byte == b'?') {
3182 fragments.push((&target[..query_start], RequestLocation::Unknown));
3183 fragments.push((&target[query_start + 1..], RequestLocation::Query));
3184 } else {
3185 fragments.push((target, RequestLocation::Unknown));
3186 }
3187}
3188
3189fn detect_http2_header_blocking_action(
3191 secrets: &[IneligibleSecret],
3192 headers: &[(Vec<u8>, Vec<u8>)],
3193 summary: &RequestSummary,
3194 stream_id: u32,
3195) -> Option<SecretViolationReport> {
3196 let mut fragments = Vec::new();
3197 let metadata: Vec<Vec<u8>> = headers
3199 .iter()
3200 .filter(|(name, _)| !name.starts_with(b":"))
3201 .map(|(name, value)| [name.as_slice(), b": ", value.as_slice()].concat())
3202 .collect();
3203 for (name, value) in headers {
3204 fragments.push((name.as_slice(), RequestLocation::Unknown));
3205 if name.eq_ignore_ascii_case(b":path") {
3206 push_request_target_fragments(&mut fragments, value);
3207 } else if name.starts_with(b":") {
3208 fragments.push((value.as_slice(), RequestLocation::Unknown));
3209 }
3210 }
3211 for line in &metadata {
3212 fragments.push((line.as_slice(), RequestLocation::Header));
3213 }
3214 detect_blocking_action_in_fragments(
3215 secrets,
3216 &fragments,
3217 RequestProtocol::Http2,
3218 summary,
3219 Some(stream_id),
3220 )
3221}
3222
3223fn detect_blocking_action_in_fragments(
3226 secrets: &[IneligibleSecret],
3227 fragments: &[(&[u8], RequestLocation)],
3228 protocol: RequestProtocol,
3229 summary: &RequestSummary,
3230 http2_stream_id: Option<u32>,
3231) -> Option<SecretViolationReport> {
3232 if secrets.is_empty() {
3233 return None;
3234 }
3235 let opaque = matches!(protocol, RequestProtocol::Opaque);
3236 let mut detected = None;
3237
3238 for &(data, location) in fragments {
3239 let url_decoded = data
3240 .contains(&b'%')
3241 .then(|| percent_decode(data).collect::<Vec<u8>>());
3242 let json_decoded = data
3243 .windows(2)
3244 .any(|window| window == b"\\u")
3245 .then(|| json_unescape(data));
3246 let basic_auth_credentials =
3247 if opaque || matches!(location, RequestLocation::Header | RequestLocation::Trailer) {
3248 decoded_basic_auth_credentials(&String::from_utf8_lossy(data))
3249 } else {
3250 Vec::new()
3251 };
3252
3253 for secret in secrets {
3254 let needle = secret.placeholder.as_bytes();
3255 let matched = if basic_auth_credentials
3256 .iter()
3257 .any(|decoded| decoded.contains(&secret.placeholder))
3258 {
3259 Some((
3260 if location == RequestLocation::Trailer {
3261 location
3262 } else {
3263 RequestLocation::BasicAuth
3264 },
3265 PlaceholderMatchForm::BasicAuthDecoded,
3266 ))
3267 } else if contains_bytes(data, needle) {
3268 Some((location, PlaceholderMatchForm::Raw))
3269 } else if url_decoded
3270 .as_deref()
3271 .is_some_and(|decoded| contains_bytes(decoded, needle))
3272 {
3273 Some((location, PlaceholderMatchForm::PercentDecoded))
3274 } else if json_decoded
3275 .as_deref()
3276 .is_some_and(|decoded| contains_bytes(decoded, needle))
3277 {
3278 Some((location, PlaceholderMatchForm::JsonUnescaped))
3279 } else {
3280 None
3281 };
3282
3283 if let Some((location, match_form)) = matched {
3284 let report = SecretViolationReport {
3285 action: secret.action,
3286 env_var: secret.env_var.clone(),
3287 placeholder: secret.placeholder.clone(),
3288 protocol,
3289 location,
3290 match_form,
3291 method: summary.method.clone(),
3292 path: summary.path.clone(),
3293 host: summary.host.clone(),
3294 http2_stream_id,
3295 };
3296
3297 detected = Some(strictest_violation_report(detected, report));
3298 }
3299 }
3300 }
3301
3302 detected
3303}
3304
3305fn is_scoped_fragment_location(location: RequestLocation) -> bool {
3308 matches!(
3309 location,
3310 RequestLocation::Body | RequestLocation::ChunkMetadata | RequestLocation::Trailer
3311 )
3312}
3313
3314fn update_tail_buffer(tail: &mut Vec<u8>, data: &[u8], tail_size: usize) {
3315 if tail_size == 0 {
3316 tail.clear();
3317 return;
3318 }
3319 if data.len() >= tail_size {
3320 tail.clear();
3321 tail.extend_from_slice(&data[data.len() - tail_size..]);
3322 return;
3323 }
3324 tail.extend_from_slice(data);
3325 let overflow = tail.len().saturating_sub(tail_size);
3326 if overflow > 0 {
3327 tail.drain(..overflow);
3328 }
3329}
3330
3331fn consume_chunked_body(
3334 state: &mut ChunkedBodyState,
3335 data: &[u8],
3336) -> Result<Option<usize>, SecretViolationAction> {
3337 process_chunked_body(state, data, |_| Ok(()))
3338}
3339
3340fn process_chunked_body<E>(
3343 state: &mut ChunkedBodyState,
3344 data: &[u8],
3345 mut on_event: E,
3346) -> Result<Option<usize>, SecretViolationAction>
3347where
3348 E: FnMut(ChunkedBodyEvent<'_>) -> Result<(), SecretViolationAction>,
3349{
3350 let mut cursor = 0;
3351 while cursor < data.len() {
3352 let phase = std::mem::replace(&mut state.phase, ChunkedPhase::SizeLine);
3353 match phase {
3354 ChunkedPhase::SizeLine => {
3355 state.line.push(data[cursor]);
3356 cursor += 1;
3357 if state.line.len() > MAX_HTTP_HEADER_BYTES {
3358 return Err(SecretViolationAction::Block);
3359 }
3360 if state.line.ends_with(b"\r\n") {
3361 let line = &state.line[..state.line.len() - 2];
3362 let size = parse_chunk_size(line)?;
3363 on_event(ChunkedBodyEvent::SizeLine(&state.line))?;
3367 state.line.clear();
3368 state.phase = if size == 0 {
3369 on_event(ChunkedBodyEvent::ZeroChunk)?;
3370 ChunkedPhase::TrailerLine
3371 } else {
3372 ChunkedPhase::Data { remaining: size }
3373 };
3374 } else {
3375 state.phase = ChunkedPhase::SizeLine;
3376 }
3377 }
3378 ChunkedPhase::Data { mut remaining } => {
3379 let take = remaining.min(data.len() - cursor);
3380 on_event(ChunkedBodyEvent::Payload(&data[cursor..cursor + take]))?;
3381 cursor += take;
3382 remaining -= take;
3383 if remaining == 0 {
3384 state.phase = ChunkedPhase::DataCrlf { seen_cr: false };
3385 } else {
3386 state.phase = ChunkedPhase::Data { remaining };
3387 }
3388 }
3389 ChunkedPhase::DataCrlf { mut seen_cr } => {
3390 if !seen_cr {
3391 if data[cursor] != b'\r' {
3392 return Err(SecretViolationAction::Block);
3393 }
3394 seen_cr = true;
3395 cursor += 1;
3396 state.phase = ChunkedPhase::DataCrlf { seen_cr };
3397 } else {
3398 if data[cursor] != b'\n' {
3399 return Err(SecretViolationAction::Block);
3400 }
3401 state.phase = ChunkedPhase::SizeLine;
3402 cursor += 1;
3403 }
3404 }
3405 ChunkedPhase::TrailerLine => {
3406 state.line.push(data[cursor]);
3407 cursor += 1;
3408 if state.line.len() > MAX_HTTP_HEADER_BYTES {
3409 return Err(SecretViolationAction::Block);
3410 }
3411 if state.line.ends_with(b"\r\n") {
3412 let is_empty = state.line.len() == 2;
3413 on_event(ChunkedBodyEvent::TrailerLine(&state.line))?;
3414 state.line.clear();
3415 if is_empty {
3416 return Ok(Some(cursor));
3417 }
3418 state.phase = ChunkedPhase::TrailerLine;
3419 } else {
3420 state.phase = ChunkedPhase::TrailerLine;
3421 }
3422 }
3423 }
3424 }
3425
3426 Ok(None)
3427}
3428
3429fn parse_chunk_size(line: &[u8]) -> Result<usize, SecretViolationAction> {
3430 let size = line
3431 .split(|byte| *byte == b';')
3432 .next()
3433 .unwrap_or_default()
3434 .trim_ascii();
3435 if size.is_empty() {
3436 return Err(SecretViolationAction::Block);
3437 }
3438 let size = std::str::from_utf8(size).map_err(|_| SecretViolationAction::Block)?;
3439 usize::from_str_radix(size, 16).map_err(|_| SecretViolationAction::Block)
3440}
3441
3442fn strictest_violation_report(
3445 current: Option<SecretViolationReport>,
3446 candidate: SecretViolationReport,
3447) -> SecretViolationReport {
3448 let Some(current) = current else {
3449 return candidate;
3450 };
3451 if candidate.action.priority() > current.action.priority() {
3452 candidate
3453 } else {
3454 current
3455 }
3456}
3457
3458impl BlockingAction {
3459 fn priority(self) -> u8 {
3460 match self {
3461 Self::Block => 0,
3462 Self::BlockAndLog => 1,
3463 Self::BlockAndTerminate => 2,
3464 }
3465 }
3466}
3467
3468impl SecretViolationReport {
3469 fn apply_request_summary(&mut self, summary: &RequestSummary) {
3470 if self.method.is_none() {
3471 self.method = summary.method.clone();
3472 }
3473 if self.path.is_none() {
3474 self.path = summary.path.clone();
3475 }
3476 if self.host.is_none() {
3477 self.host = summary.host.clone();
3478 }
3479 }
3480}
3481
3482#[cfg(test)]
3487mod tests {
3488 use super::*;
3489 use crate::engine::secrets::hpack::tests::{PANICKING_BLOCKS, Rng, random_block};
3490 use crate::netstack::shared::{ResolvedHostnameFamily, SharedState};
3491 use crate::secrets::config::SecretSubstitution;
3492 use microsandbox_types::compat;
3493
3494 use std::net::{IpAddr, Ipv4Addr};
3495 use std::time::Duration;
3496
3497 #[test]
3498 fn legacy_header_scopes_merge_for_http1_and_http2() {
3499 for headers in [false, true] {
3500 for basic_auth in [false, true] {
3501 let mut wire = serde_json::json!({"on_violation":"block", "secrets":[{
3502 "env_var":"KEY", "value":"real-secret", "placeholder":"$KEY",
3503 "allowed_hosts":[{"exact":"api.example.com"}],
3504 "injection":{"headers":headers,"basic_auth":basic_auth,"query_params":true},
3505 "passthrough_hosts":[{"exact":"api.example.com"}],
3506 "require_tls_identity":false
3507 }]});
3508 compat::v0_5_0::local::secrets::to_current(wire.as_object_mut().unwrap()).unwrap();
3509 let config: SecretsConfig = serde_json::from_value(wire).unwrap();
3510 let headers = headers || basic_auth;
3511 let basic = BASE64.encode("user:$KEY");
3512 let input = format!(
3513 "GET /?key=$KEY HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Basic {basic}\r\nX-Key: $KEY\r\n\r\n"
3514 );
3515 for tls in [false, true] {
3516 let mut handler = SecretsHandler::new(&config, "api.example.com", tls);
3517 let output = handler.substitute(input.as_bytes()).unwrap();
3518 let output = String::from_utf8(output.into_owned()).unwrap();
3519 let expected_basic = BASE64.encode(if headers {
3520 "user:real-secret"
3521 } else {
3522 "user:$KEY"
3523 });
3524 assert!(
3525 output.contains(&format!("Authorization: Basic {expected_basic}")),
3526 "{output}"
3527 );
3528 assert!(
3529 output.contains(if headers {
3530 "X-Key: real-secret"
3531 } else {
3532 "X-Key: $KEY"
3533 }),
3534 "{output}"
3535 );
3536 assert!(output.contains("/?key=real-secret"), "{output}");
3537 let mut h2 = vec![
3538 (b":path".to_vec(), b"/?key=$KEY".to_vec()),
3539 (
3540 b"authorization".to_vec(),
3541 format!("Basic {basic}").into_bytes(),
3542 ),
3543 (b"x-key".to_vec(), b"$KEY".to_vec()),
3544 ];
3545 handler.substitute_http2_headers(&mut h2);
3546 assert_eq!(h2[1].1, format!("Basic {expected_basic}").as_bytes());
3547 assert_eq!(
3548 h2[2].1,
3549 if headers {
3550 b"real-secret".as_slice()
3551 } else {
3552 b"$KEY".as_slice()
3553 }
3554 );
3555 }
3556 let mut denied = SecretsHandler::new(&config, "denied.example", true);
3557 assert!(denied.substitute(input.as_bytes()).is_err());
3558 }
3559 }
3560 }
3561
3562 #[test]
3563 fn decoded_v06_policy_uses_current_disabled_body_enforcement() {
3564 let mut wire = serde_json::json!({"on_violation":"block", "secrets":[{
3565 "env_var":"KEY", "value":"real-secret", "placeholder":"$KEY",
3566 "allowed_hosts":[{"exact":"api.example.com"}],
3567 "injection":{"headers":true,"basic_auth":true,"query_params":false,"body":false},
3568 "require_tls_identity":false
3569 }]});
3570 compat::v0_5_0::local::secrets::to_current(wire.as_object_mut().unwrap()).unwrap();
3571 let config: SecretsConfig = serde_json::from_value(wire).unwrap();
3572 let request = b"POST / HTTP/1.1\r\nHost: api.example.com\r\nContent-Length: 4\r\n\r\n$KEY";
3573 let mut handler = SecretsHandler::new(&config, "api.example.com", true);
3574 assert_eq!(handler.substitute(request).unwrap().as_ref(), request);
3575 let mut denied = SecretsHandler::new(&config, "denied.example", true);
3576 assert_eq!(
3577 denied.substitute(request).unwrap_err(),
3578 SecretViolationAction::Block
3579 );
3580 }
3581
3582 #[test]
3583 fn global_passthrough_preserves_fallback_and_per_secret_overrides() {
3584 let input = b"GET / HTTP/1.1\r\nX-Key: $KEY\r\n\r\n";
3585 let mut secret = make_secret("$KEY", "real-secret", "allowed.example");
3586 secret.passthrough_hosts = vec![HostPattern::Exact("entry.example".into())];
3587 let mut config = make_config(vec![secret]);
3588 config.passthrough_hosts = Some(vec![HostPattern::Exact("global.example".into())]);
3589 config.violation_action = SecretViolationAction::BlockAndLog;
3590 for host in ["entry.example", "global.example"] {
3591 let mut handler = SecretsHandler::new(&config, host, true);
3592 assert_eq!(handler.substitute(input).unwrap().as_ref(), input);
3593 }
3594 let mut denied = SecretsHandler::new(&config, "denied.example", true);
3595 assert_eq!(
3596 denied.substitute(input).unwrap_err(),
3597 SecretViolationAction::BlockAndLog
3598 );
3599 config.secrets[0].passthrough_hosts.clear();
3600 config.secrets[0].violation_action = Some(SecretViolationAction::BlockAndTerminate);
3601 let mut overridden = SecretsHandler::new(&config, "global.example", true);
3602 assert_eq!(
3603 overridden.substitute(input).unwrap_err(),
3604 SecretViolationAction::BlockAndTerminate
3605 );
3606 config.secrets[0].violation_action = None;
3608 let mut inherited = SecretsHandler::new(&config, "global.example", true);
3609 assert_eq!(inherited.substitute(input).unwrap().as_ref(), input);
3610 }
3611
3612 fn make_config(secrets: Vec<SecretEntry>) -> SecretsConfig {
3613 SecretsConfig {
3614 passthrough_hosts: None,
3615 secrets,
3616 violation_action: SecretViolationAction::Block,
3617 }
3618 }
3619
3620 fn make_secret(placeholder: &str, value: &str, host: &str) -> SecretEntry {
3621 SecretEntry {
3622 env_var: "TEST_KEY".into(),
3623 value: zeroize::Zeroizing::new(value.into()),
3624 source: None,
3625 placeholder: placeholder.into(),
3626 allowed_hosts: vec![HostPattern::Exact(host.into())],
3627 substitution: SecretSubstitution::default(),
3628 passthrough_hosts: Vec::new(),
3629 violation_action: None,
3630 require_tls_identity: true,
3631 }
3632 }
3633
3634 fn make_passthrough_secret(placeholder: &str, value: &str, host: &str) -> SecretEntry {
3635 let mut secret = make_secret(placeholder, value, host);
3636 secret.passthrough_hosts = vec![HostPattern::Exact(host.into())];
3637 secret
3638 }
3639
3640 fn cache_host(shared: &SharedState, host: &str, ip: Ipv4Addr) {
3641 shared.cache_resolved_hostname(
3642 host,
3643 ResolvedHostnameFamily::Ipv4,
3644 [IpAddr::V4(ip)],
3645 Duration::from_secs(60),
3646 );
3647 }
3648
3649 fn basic_auth_only() -> SecretSubstitution {
3650 SecretSubstitution {
3651 headers: true,
3652 header_fields: Vec::new(),
3653 query: false,
3654 body: false,
3655 }
3656 }
3657
3658 fn plain_http_policy_handler(config: &SecretsConfig) -> SecretsHandler {
3659 let ip = Ipv4Addr::new(203, 0, 113, 10);
3660 let shared = Arc::new(SharedState::new(16));
3661 cache_host(&shared, "a.example", ip);
3662 cache_host(&shared, "b.example", ip);
3663 SecretsHandler::new_plain_http_policy(
3664 config,
3665 "a.example",
3666 SocketAddr::new(IpAddr::V4(ip), 80),
3667 Arc::new(NetworkPolicy::allow_all()),
3668 shared,
3669 )
3670 }
3671
3672 #[test]
3673 fn plain_http_policy_keeps_secret_identity_across_allowed_host_switch() {
3674 let mut secret = make_secret("$KEY", "real-secret", "a.example");
3675 secret.require_tls_identity = false;
3676 let config = make_config(vec![secret]);
3677 let mut handler = plain_http_policy_handler(&config);
3678 let first = handler
3679 .substitute(b"GET /one HTTP/1.1\r\nHost: a.example\r\nAuth: $KEY\r\n\r\n")
3680 .unwrap();
3681 assert!(String::from_utf8_lossy(&first).contains("Auth: real-secret"));
3682
3683 assert!(
3686 handler
3687 .substitute(b"GET\t/two HTTP/1.1\r\nHost: b.exam")
3688 .unwrap()
3689 .is_empty()
3690 );
3691 assert_eq!(
3692 handler
3693 .substitute(b"ple\r\nAuth: $KEY\r\n\r\n")
3694 .unwrap_err(),
3695 SecretViolationAction::Block
3696 );
3697 }
3698
3699 #[test]
3700 fn plain_http_policy_keeps_passthrough_identity_across_host_switch() {
3701 let mut secret = make_secret("$KEY", "real-secret", "secret.example");
3702 secret.passthrough_hosts = vec![HostPattern::Exact("a.example".into())];
3703 let config = make_config(vec![secret]);
3704 let mut handler = plain_http_policy_handler(&config);
3705 let first = b"GET /one HTTP/1.1\r\nHost: a.example\r\nAuth: $KEY\r\n\r\n";
3706 assert_eq!(handler.substitute(first).unwrap().as_ref(), first);
3707 assert_eq!(
3708 handler
3709 .substitute(b"GET /two HTTP/1.1\r\nHost: b.example\r\nAuth: $KEY\r\n\r\n",)
3710 .unwrap_err(),
3711 SecretViolationAction::Block
3712 );
3713 }
3714
3715 #[test]
3716 fn plain_http_policy_preserves_secret_free_host_switches() {
3717 let mut handler = plain_http_policy_handler(&SecretsConfig::default());
3718 let pipeline = b"GET /one HTTP/1.1\r\nHost: a.example\r\n\r\nGET /two HTTP/1.1\r\nHost: b.example\r\n\r\n";
3719 assert_eq!(handler.substitute(pipeline).unwrap().as_ref(), pipeline);
3720 }
3721
3722 #[test]
3723 fn plain_http_policy_preserves_host_agnostic_secret_switches() {
3724 let mut secret = make_secret("$KEY", "real-secret", "a.example");
3725 secret.allowed_hosts = vec![HostPattern::Any];
3726 secret.require_tls_identity = false;
3727 let config = make_config(vec![secret]);
3728 let mut handler = plain_http_policy_handler(&config);
3729 let second = handler
3730 .substitute(b"GET / HTTP/1.1\r\nHost: b.example\r\nAuth: $KEY\r\n\r\n")
3731 .unwrap();
3732 assert!(String::from_utf8_lossy(&second).contains("Auth: real-secret"));
3733 }
3734
3735 #[test]
3736 fn plain_http_policy_keeps_secret_identity_for_http2_authority() {
3737 let mut secret = make_secret("$KEY", "real-secret", "a.example");
3738 secret.require_tls_identity = false;
3739 let config = make_config(vec![secret]);
3740 let mut handler = plain_http_policy_handler(&config);
3741 let request = h2_request(
3742 &[
3743 (b":method", b"GET"),
3744 (b":scheme", b"http"),
3745 (b":authority", b"b.example"),
3746 (b":path", b"/"),
3747 (b"authorization", b"Bearer $KEY"),
3748 ],
3749 true,
3750 );
3751 assert_eq!(
3752 handler.substitute(&request).unwrap_err(),
3753 SecretViolationAction::Block
3754 );
3755 }
3756
3757 fn split_http_body(data: &[u8]) -> (&[u8], &[u8]) {
3758 let boundary = find_header_boundary(data).expect("HTTP header boundary");
3759 data.split_at(boundary)
3760 }
3761
3762 fn decode_chunked_payload(data: &[u8]) -> (Vec<u8>, Vec<u8>, usize) {
3763 let mut cursor = 0;
3764 let mut decoded = Vec::new();
3765 let mut trailers = Vec::new();
3766
3767 loop {
3768 let line_end = data[cursor..]
3769 .windows(2)
3770 .position(|window| window == b"\r\n")
3771 .map(|pos| cursor + pos)
3772 .expect("chunk size line");
3773 let size = parse_chunk_size(&data[cursor..line_end]).expect("valid chunk size");
3774 cursor = line_end + 2;
3775
3776 if size == 0 {
3777 loop {
3778 let trailer_end = data[cursor..]
3779 .windows(2)
3780 .position(|window| window == b"\r\n")
3781 .map(|pos| cursor + pos + 2)
3782 .expect("trailer line");
3783 trailers.extend_from_slice(&data[cursor..trailer_end]);
3784 let empty = trailer_end - cursor == 2;
3785 cursor = trailer_end;
3786 if empty {
3787 return (decoded, trailers, cursor);
3788 }
3789 }
3790 }
3791
3792 decoded.extend_from_slice(&data[cursor..cursor + size]);
3793 cursor += size;
3794 assert_eq!(&data[cursor..cursor + 2], b"\r\n");
3795 cursor += 2;
3796 }
3797 }
3798
3799 fn encode_h2_header_block(headers: &[(&[u8], &[u8])]) -> Vec<u8> {
3800 let mut encoder = HpackEncoder::with_dynamic_size(4096);
3801 let mut block = Vec::new();
3802 for (name, value) in headers {
3803 encoder
3804 .encode(
3805 (name.to_vec(), value.to_vec(), HpackEncoder::NEVER_INDEXED),
3806 &mut block,
3807 )
3808 .unwrap();
3809 }
3810 block
3811 }
3812
3813 fn h2_request(headers: &[(&[u8], &[u8])], end_stream: bool) -> Vec<u8> {
3814 let encoded = encode_h2_header_block(headers);
3815 let mut out = HTTP2_PREFACE.to_vec();
3816 append_http2_frame(&mut out, 0x4, 0, 0, &[]).unwrap();
3817 append_http2_header_frames(&mut out, 1, end_stream, &encoded).unwrap();
3818 out
3819 }
3820
3821 fn h2_request_with_split_headers(headers: &[(&[u8], &[u8])], split_at: usize) -> Vec<u8> {
3822 let encoded = encode_h2_header_block(headers);
3823 let split_at = split_at.min(encoded.len());
3824 let mut out = HTTP2_PREFACE.to_vec();
3825 append_http2_frame(&mut out, 0x4, 0, 0, &[]).unwrap();
3826 append_http2_frame(&mut out, HTTP2_FRAME_HEADERS, 0, 1, &encoded[..split_at]).unwrap();
3827 append_http2_frame(
3828 &mut out,
3829 HTTP2_FRAME_CONTINUATION,
3830 HTTP2_FLAG_END_HEADERS | HTTP2_FLAG_END_STREAM,
3831 1,
3832 &encoded[split_at..],
3833 )
3834 .unwrap();
3835 out
3836 }
3837
3838 fn h2_request_with_data(headers: &[(&[u8], &[u8])], data: &[u8]) -> Vec<u8> {
3839 let mut out = h2_request(headers, false);
3840 append_http2_frame(&mut out, HTTP2_FRAME_DATA, HTTP2_FLAG_END_STREAM, 1, data).unwrap();
3841 out
3842 }
3843
3844 fn append_h2_headers(
3845 out: &mut Vec<u8>,
3846 stream_id: u32,
3847 headers: &[(&[u8], &[u8])],
3848 end_stream: bool,
3849 ) {
3850 let encoded = encode_h2_header_block(headers);
3851 append_http2_header_frames(out, stream_id, end_stream, &encoded).unwrap();
3852 }
3853
3854 fn decode_first_h2_headers(data: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
3855 assert!(data.starts_with(HTTP2_PREFACE));
3856 let mut cursor = HTTP2_PREFACE.len();
3857 let mut decoder = HpackDecoder::with_dynamic_size(4096);
3858 let mut header_block = Vec::new();
3859 let mut in_headers = false;
3860
3861 while cursor + 9 <= data.len() {
3862 let len = http2_frame_payload_len(&data[cursor..cursor + 9]);
3863 let raw = &data[cursor..cursor + 9 + len];
3864 cursor += 9 + len;
3865 let frame = parse_http2_frame(raw).unwrap();
3866 match frame.kind {
3867 HTTP2_FRAME_HEADERS => {
3868 header_block.extend_from_slice(
3869 http2_headers_fragment(frame.flags, frame.payload).unwrap(),
3870 );
3871 if frame.flags & HTTP2_FLAG_END_HEADERS != 0 {
3872 break;
3873 }
3874 in_headers = true;
3875 }
3876 HTTP2_FRAME_CONTINUATION if in_headers => {
3877 header_block.extend_from_slice(frame.payload);
3878 if frame.flags & HTTP2_FLAG_END_HEADERS != 0 {
3879 break;
3880 }
3881 }
3882 _ => {}
3883 }
3884 }
3885
3886 let mut encoded = header_block;
3887 let mut headers = Vec::new();
3888 decoder.decode(&mut encoded, &mut headers).unwrap();
3889 headers
3890 .into_iter()
3891 .map(|(name, value, _flags)| (name, value))
3892 .collect()
3893 }
3894
3895 fn h2_header_value(headers: &[(Vec<u8>, Vec<u8>)], name: &[u8]) -> String {
3896 let value = headers
3897 .iter()
3898 .find(|(header_name, _)| header_name.eq_ignore_ascii_case(name))
3899 .map(|(_, value)| value.as_slice())
3900 .expect("header present");
3901 String::from_utf8(value.to_vec()).unwrap()
3902 }
3903
3904 #[test]
3905 fn violation_report_includes_secret_and_basic_auth_context() {
3906 let secret = IneligibleSecret {
3907 env_var: "OPENAI_API_KEY".into(),
3908 placeholder: "$KEY".into(),
3909 action: BlockingAction::BlockAndLog,
3910 };
3911 let encoded = BASE64.encode(b"user:$KEY");
3912 let headers = format!(
3913 "POST /v1/chat/completions?token=redacted HTTP/1.1\r\nHost: evil.example.com\r\nAuthorization: Basic {encoded}\r\n\r\n"
3914 );
3915
3916 let report = detect_blocking_action_with_tail(
3917 &[secret],
3918 &[],
3919 headers.as_bytes(),
3920 &headers,
3921 RequestProtocol::Http1,
3922 RequestLocation::Unknown,
3923 None,
3924 )
3925 .expect("violation report");
3926
3927 assert_eq!(report.action, BlockingAction::BlockAndLog);
3928 assert_eq!(report.env_var, "OPENAI_API_KEY");
3929 assert_eq!(report.placeholder, "$KEY");
3930 assert_eq!(report.location, RequestLocation::BasicAuth);
3931 assert!(matches!(
3932 report.match_form,
3933 PlaceholderMatchForm::BasicAuthDecoded
3934 ));
3935 assert_eq!(report.method.as_deref(), Some("POST"));
3936 assert_eq!(report.path.as_deref(), Some("/v1/chat/completions"));
3937 assert_eq!(report.host.as_deref(), Some("evil.example.com"));
3938 }
3939
3940 #[test]
3941 fn violation_report_classifies_percent_decoded_query_match() {
3942 let secret = IneligibleSecret {
3943 env_var: "SERVICE_TOKEN".into(),
3944 placeholder: "abc/key".into(),
3945 action: BlockingAction::BlockAndLog,
3946 };
3947 let headers =
3948 "GET /leak?token=abc%2Fkey&other=redacted HTTP/1.1\r\nHost: evil.example.com\r\n\r\n";
3949
3950 let report = detect_blocking_action_with_tail(
3951 &[secret],
3952 &[],
3953 headers.as_bytes(),
3954 headers,
3955 RequestProtocol::Http1,
3956 RequestLocation::Unknown,
3957 None,
3958 )
3959 .expect("violation report");
3960
3961 assert_eq!(report.env_var, "SERVICE_TOKEN");
3962 assert_eq!(report.location, RequestLocation::Query);
3963 assert!(matches!(
3964 report.match_form,
3965 PlaceholderMatchForm::PercentDecoded
3966 ));
3967 assert_eq!(report.method.as_deref(), Some("GET"));
3968 assert_eq!(report.path.as_deref(), Some("/leak"));
3969 assert_eq!(report.host.as_deref(), Some("evil.example.com"));
3970 }
3971
3972 #[test]
3973 fn http1_harmless_encoding_preserves_substitution_across_reads() {
3974 for body in [
3975 r#"{"x":"plain"}"#,
3976 r#"{"x":"100%"}"#,
3977 r#"{"x":"a%20b"}"#,
3978 r#"{"x":"\u0041"}"#,
3979 r#"{"x":"\\user"}"#,
3980 ] {
3981 for query in [false, true] {
3982 let mut secret = make_secret("$KEY", "real-secret", "api.example.com");
3983 secret.substitution.headers = !query;
3984 secret.substitution.query = query;
3985 let config = make_config(vec![secret]);
3986 let target = if query { "/?key=$KEY" } else { "/" };
3987 let auth = if query {
3988 ""
3989 } else {
3990 "Authorization: Bearer $KEY\r\n"
3991 };
3992 let request = format!(
3993 "POST {target} HTTP/1.1\r\nHost: api.example.com\r\n{auth}Content-Length: {}\r\n\r\n{body}",
3994 body.len()
3995 );
3996 let expected = request.replace("$KEY", "real-secret");
3997 for split in 0..=request.len() {
3998 let mut handler = SecretsHandler::new(&config, "api.example.com", true);
3999 let mut output = Vec::new();
4000 for bytes in [&request.as_bytes()[..split], &request.as_bytes()[split..]] {
4001 if bytes.is_empty() {
4002 continue;
4003 }
4004 output.extend_from_slice(&handler.substitute(bytes).unwrap_or_else(
4005 |action| {
4006 panic!("body={body:?}, query={query}, split={split}: {action:?}")
4007 },
4008 ));
4009 }
4010 assert_eq!(output, expected.as_bytes(), "body={body:?}, split={split}");
4011 }
4012 }
4013 }
4014 }
4015
4016 #[test]
4017 fn http1_every_body_byte_preserves_header_substitution() {
4018 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.example.com")]);
4019 for byte in 0..=u8::MAX {
4020 let bodies = [
4022 vec![byte],
4023 format!("%{byte:02X}").into_bytes(),
4024 format!(r"\u{byte:04x}").into_bytes(),
4025 ];
4026 for body in bodies {
4027 let headers = format!(
4028 "POST / HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer $KEY\r\nContent-Length: {}\r\n\r\n",
4029 body.len()
4030 );
4031 let mut request = headers.as_bytes().to_vec();
4032 request.extend_from_slice(&body);
4033 let mut expected = headers.replace("$KEY", "real-secret").into_bytes();
4034 expected.extend_from_slice(&body);
4035 for split in 0..=request.len() {
4036 let mut handler = SecretsHandler::new(&config, "api.example.com", true);
4037 let mut output = Vec::new();
4038 for bytes in [&request[..split], &request[split..]] {
4039 if !bytes.is_empty() {
4040 output.extend_from_slice(&handler.substitute(bytes).unwrap_or_else(
4041 |action| {
4042 panic!(
4043 "byte={byte:02x}, body={body:?}, split={split}: {action:?}"
4044 )
4045 },
4046 ));
4047 }
4048 }
4049 assert_eq!(
4050 output, expected,
4051 "byte={byte:02x}, body={body:?}, split={split}"
4052 );
4053 }
4054 }
4055 }
4056 }
4057
4058 #[test]
4059 fn http1_unrelated_header_and_query_characters_preserve_substitution() {
4060 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.example.com")]);
4061 for byte in 0..=u8::MAX {
4062 let note = if (b' '..=b'~').contains(&byte) {
4064 char::from(byte).to_string()
4065 } else {
4066 format!(r"\u{byte:04x}")
4067 };
4068 let request = format!(
4069 "GET /?note=%{byte:02X} HTTP/1.1\r\nHost: api.example.com\r\nAuthorization: Bearer $KEY\r\nX-Note: {note}\r\n\r\n"
4070 );
4071 let mut handler = SecretsHandler::new(&config, "api.example.com", true);
4072 assert_eq!(
4073 handler.substitute(request.as_bytes()).unwrap().as_ref(),
4074 request.replace("$KEY", "real-secret").as_bytes(),
4075 "byte={byte:02x}"
4076 );
4077 }
4078 }
4079
4080 #[test]
4081 fn http1_allowed_header_cannot_mask_forbidden_encoded_locations() {
4082 let basic = format!("Authorization: Basic {}\r\n", BASE64.encode(b"user:$KEY"));
4083 let mut notes = vec![
4084 Vec::new(),
4085 b"X-Note: %20\r\n".to_vec(),
4086 b"X-Note: \\u0041\r\n".to_vec(),
4087 ];
4088 notes.extend(
4090 (0x80..=u8::MAX).map(|byte| [b"X-Note: ".as_slice(), &[byte], b"\r\n"].concat()),
4091 );
4092 for auth in ["Authorization: Bearer $KEY\r\n", basic.as_str()] {
4093 for body in ["$BLOCKED", "%24BLOCKED", r"\u0024BLOCKED"] {
4094 for note in ¬es {
4095 let config = make_config(vec![
4096 make_secret("$KEY", "real-secret", "api.example.com"),
4097 make_secret("$BLOCKED", "other-secret", "other.example"),
4098 ]);
4099 let mut request =
4100 format!("POST / HTTP/1.1\r\nHost: api.example.com\r\n{auth}").into_bytes();
4101 request.extend_from_slice(note);
4102 request.extend_from_slice(
4103 format!("Content-Length: {}\r\n\r\n{body}", body.len()).as_bytes(),
4104 );
4105 for split in 0..=request.len() {
4106 let mut handler = SecretsHandler::new(&config, "api.example.com", true);
4107 let mut blocked = false;
4108 for bytes in [&request[..split], &request[split..]] {
4109 if bytes.is_empty() {
4110 continue;
4111 }
4112 if let Err(action) = handler.substitute(bytes) {
4113 assert_eq!(action, SecretViolationAction::Block);
4114 blocked = true;
4115 break;
4116 }
4117 }
4118 assert!(
4119 blocked,
4120 "auth={auth:?}, note={note:?}, body={body:?}, split={split}"
4121 );
4122 }
4123 }
4124 }
4125 }
4126 }
4127
4128 #[test]
4129 fn http_request_locations_enforce_the_same_policy_in_both_protocols() {
4130 for target in ["/", "/$KEY", "/%24KEY", "/?key=$KEY", "/?key=%24KEY"] {
4131 for header_value in ["plain", "$KEY", "%24KEY"] {
4132 for headers in [false, true] {
4133 for query in [false, true] {
4134 let mut secret = make_secret("$KEY", "real-secret", "api.example.com");
4135 secret.substitution.headers = headers;
4136 secret.substitution.query = query;
4137 secret.substitution.body = true;
4138 let config = make_config(vec![secret]);
4139 let expected_target = match target.split_once('?') {
4140 Some((path, value)) if query => {
4141 format!("{path}?{}", value.replace("$KEY", "real-secret"))
4142 }
4143 _ => target.to_string(),
4144 };
4145 let expected_header = if headers {
4146 header_value.replace("$KEY", "real-secret")
4147 } else {
4148 header_value.to_string()
4149 };
4150 for http2 in [false, true] {
4151 let mut handler = SecretsHandler::new(&config, "api.example.com", true);
4152 let request = if http2 {
4153 h2_request(
4154 &[
4155 (b":method", b"GET"),
4156 (b":scheme", b"https"),
4157 (b":authority", b"api.example.com"),
4158 (b":path", target.as_bytes()),
4159 (b"x-key", header_value.as_bytes()),
4160 ],
4161 true,
4162 )
4163 } else {
4164 format!("GET {target} HTTP/1.1\r\nHost: api.example.com\r\nX-Key: {header_value}\r\n\r\n").into_bytes()
4165 };
4166 let output = handler.substitute(&request).unwrap();
4167 if http2 {
4168 let fields = decode_first_h2_headers(&output);
4169 assert_eq!(h2_header_value(&fields, b":path"), expected_target);
4170 assert_eq!(h2_header_value(&fields, b"x-key"), expected_header);
4171 } else {
4172 assert_eq!(output.as_ref(), format!("GET {expected_target} HTTP/1.1\r\nHost: api.example.com\r\nX-Key: {expected_header}\r\n\r\n").as_bytes());
4173 }
4174 }
4175 }
4176 }
4177 }
4178 }
4179 }
4180 #[test]
4181 fn substitute_in_headers() {
4182 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4183 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4184
4185 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4186 let output = handler.substitute(input).unwrap();
4187 assert_eq!(
4188 String::from_utf8(output.into_owned()).unwrap(),
4189 "GET / HTTP/1.1\r\nAuthorization: Bearer real-secret\r\n\r\n"
4190 );
4191 }
4192
4193 #[test]
4194 fn header_field_allowlist_scopes_substitution() {
4195 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4196 secret.substitution.header_fields = vec!["authorization".into()];
4197 let config = make_config(vec![secret]);
4198 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4199
4200 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\nX-Trace: redacted\r\n\r\n";
4201 let output = handler.substitute(input).unwrap();
4202 assert_eq!(
4203 String::from_utf8(output.into_owned()).unwrap(),
4204 "GET / HTTP/1.1\r\nAuthorization: Bearer real-secret\r\nX-Trace: redacted\r\n\r\n"
4205 );
4206 }
4207
4208 #[test]
4209 fn header_field_allowlist_forwards_placeholder_in_other_headers() {
4210 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4213 secret.substitution.header_fields = vec!["authorization".into()];
4214 let config = make_config(vec![secret]);
4215 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4216
4217 let input = b"GET / HTTP/1.1\r\nX-Other: $KEY\r\nAuthorization: Bearer redacted\r\n\r\n";
4218 let output = handler.substitute(input).unwrap();
4219 assert_eq!(output.as_ref(), input);
4220 }
4221
4222 #[test]
4223 fn empty_header_field_allowlist_substitutes_every_header() {
4224 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4225 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4226
4227 let input = b"GET / HTTP/1.1\r\nX-Custom: $KEY\r\n\r\n";
4228 let output = handler.substitute(input).unwrap();
4229 assert_eq!(
4230 String::from_utf8(output.into_owned()).unwrap(),
4231 "GET / HTTP/1.1\r\nX-Custom: real-secret\r\n\r\n"
4232 );
4233 }
4234
4235 #[test]
4236 fn header_field_allowlist_scopes_basic_auth() {
4237 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4238 secret.substitution.header_fields = vec!["authorization".into()];
4239 let config = make_config(vec![secret]);
4240 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4241
4242 let encoded = BASE64.encode(b"user:$KEY");
4243 let input = format!("GET / HTTP/1.1\r\nAuthorization: Basic {encoded}\r\n\r\n");
4244 let output = handler.substitute(input.as_bytes()).unwrap();
4245 let auth = String::from_utf8(output.into_owned())
4246 .unwrap()
4247 .split("Authorization: Basic ")
4248 .nth(1)
4249 .unwrap()
4250 .trim()
4251 .to_string();
4252 assert_eq!(BASE64.decode(auth).unwrap(), b"user:real-secret");
4253 }
4254
4255 #[test]
4256 fn header_field_allowlist_forwards_basic_auth_when_authorization_excluded() {
4257 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4260 secret.substitution.header_fields = vec!["x-api-key".into()];
4261 let config = make_config(vec![secret]);
4262 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4263
4264 let encoded = BASE64.encode(b"user:$KEY");
4265 let input = format!("GET / HTTP/1.1\r\nAuthorization: Basic {encoded}\r\n\r\n");
4266 let output = handler.substitute(input.as_bytes()).unwrap();
4267 assert_eq!(output.as_ref(), input.as_bytes());
4268 }
4269
4270 #[test]
4271 fn header_field_allowlist_scopes_http2_substitution() {
4272 let ip = Ipv4Addr::new(203, 0, 113, 60);
4273 let shared = SharedState::new(16);
4274 cache_host(&shared, "api.openai.com", ip);
4275 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4276 secret.substitution.header_fields = vec!["authorization".into()];
4277 let config = make_config(vec![secret]);
4278
4279 let allowed = h2_request(
4280 &[
4281 (b":method", b"GET"),
4282 (b":scheme", b"https"),
4283 (b":authority", b"api.openai.com"),
4284 (b":path", b"/"),
4285 (b"authorization", b"Bearer $KEY"),
4286 ],
4287 true,
4288 );
4289 let mut handler =
4290 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4291 let output = handler.substitute(&allowed).unwrap().into_owned();
4292 let headers = decode_first_h2_headers(&output);
4293 assert_eq!(
4294 h2_header_value(&headers, b"authorization"),
4295 "Bearer real-secret"
4296 );
4297
4298 let forwarded = h2_request(
4299 &[
4300 (b":method", b"GET"),
4301 (b":scheme", b"https"),
4302 (b":authority", b"api.openai.com"),
4303 (b":path", b"/"),
4304 (b"x-trace", b"$KEY"),
4305 ],
4306 true,
4307 );
4308 let mut handler =
4309 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4310 let output = handler.substitute(&forwarded).unwrap().into_owned();
4311 let headers = decode_first_h2_headers(&output);
4312 assert_eq!(h2_header_value(&headers, b"x-trace"), "$KEY");
4313 }
4314
4315 #[test]
4316 fn tls_intercepted_http2_encoded_query_is_forwarded_when_query_scope_disabled() {
4317 let ip = Ipv4Addr::new(203, 0, 113, 61);
4318 let shared = SharedState::new(16);
4319 cache_host(&shared, "api.openai.com", ip);
4320 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4321 let mut handler =
4322 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4323
4324 let request = h2_request(
4328 &[
4329 (b":method", b"GET"),
4330 (b":scheme", b"https"),
4331 (b":authority", b"api.openai.com"),
4332 (b":path", b"/?token=%24KEY"),
4333 ],
4334 true,
4335 );
4336 let output = handler.substitute(&request).unwrap().into_owned();
4337 let headers = decode_first_h2_headers(&output);
4338 assert_eq!(h2_header_value(&headers, b":path"), "/?token=%24KEY");
4339 }
4340
4341 #[test]
4342 fn no_substitute_for_wrong_host() {
4343 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4344 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4345
4346 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4347 assert_eq!(
4348 handler.substitute(input).unwrap_err(),
4349 SecretViolationAction::Block
4350 );
4351 }
4352
4353 #[test]
4354 fn split_http1_post_is_not_misclassified_as_http2_preface() {
4355 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4356 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4357
4358 assert_eq!(handler.substitute(b"P").unwrap().as_ref(), b"");
4359
4360 let output = handler
4361 .substitute(b"OST / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n")
4362 .unwrap();
4363 assert_eq!(
4364 String::from_utf8(output.into_owned()).unwrap(),
4365 "POST / HTTP/1.1\r\nAuthorization: Bearer real-secret\r\n\r\n"
4366 );
4367 }
4368
4369 #[test]
4370 fn allowed_placeholder_substitutes_when_another_secret_is_ineligible() {
4371 let allowed = make_secret("$ALLOWED", "allowed-secret", "api.openai.com");
4372 let blocked = make_secret("$BLOCKED", "blocked-secret", "api.github.com");
4373 let config = make_config(vec![allowed, blocked]);
4374 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4375
4376 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $ALLOWED\r\n\r\n";
4377 let output = handler.substitute(input).unwrap();
4378
4379 assert_eq!(
4380 String::from_utf8(output.into_owned()).unwrap(),
4381 "GET / HTTP/1.1\r\nAuthorization: Bearer allowed-secret\r\n\r\n"
4382 );
4383 }
4384
4385 #[test]
4386 fn same_placeholder_substitutes_when_duplicate_secret_is_ineligible() {
4387 let allowed = make_secret("$KEY", "real-secret", "api.github.com");
4388 let mut duplicate_host = make_secret("$KEY", "real-secret", "unused.example.com");
4389 duplicate_host.allowed_hosts =
4390 vec![HostPattern::Wildcard("*.githubusercontent.com".into())];
4391 let config = make_config(vec![allowed, duplicate_host]);
4392 let mut handler = SecretsHandler::new(&config, "api.github.com", true);
4393
4394 let input =
4395 b"POST /user HTTP/1.1\r\nAuthorization: Bearer $KEY\r\nContent-Length: 4\r\n\r\n$KEY";
4396 let output = handler.substitute(input).unwrap();
4397
4398 assert_eq!(
4399 String::from_utf8(output.into_owned()).unwrap(),
4400 "POST /user HTTP/1.1\r\nAuthorization: Bearer real-secret\r\nContent-Length: 4\r\n\r\n$KEY"
4401 );
4402 }
4403
4404 #[test]
4405 fn passthrough_host_forwards_placeholder_unchanged() {
4406 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4407 secret.passthrough_hosts = vec![HostPattern::Exact("api.anthropic.com".into())];
4408 let config = make_config(vec![secret]);
4409 let mut handler = SecretsHandler::new(&config, "api.anthropic.com", true);
4410
4411 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4412 let output = handler.substitute(input).unwrap();
4413 assert_eq!(&*output, input);
4414 }
4415
4416 #[test]
4417 fn per_secret_passthrough_host_forwards_placeholder_unchanged() {
4418 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4419 secret.passthrough_hosts = vec![HostPattern::Exact("api.anthropic.com".into())];
4420 let config = make_config(vec![secret]);
4421 let mut handler = SecretsHandler::new(&config, "api.anthropic.com", true);
4422
4423 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4424 let output = handler.substitute(input).unwrap();
4425 assert_eq!(&*output, input);
4426 }
4427
4428 #[test]
4429 fn any_host_passthrough_forwards_disallowed_placeholder_unchanged() {
4430 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4431 secret.passthrough_hosts = vec![HostPattern::Any];
4432 let config = make_config(vec![secret]);
4433 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4434
4435 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4436 let output = handler.substitute(input).unwrap();
4437 assert_eq!(&*output, input);
4438 }
4439
4440 #[test]
4441 fn passthrough_only_connection_has_no_handler_work() {
4442 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4443 secret.passthrough_hosts = vec![HostPattern::Any];
4444 let config = make_config(vec![secret]);
4445 let handler = SecretsHandler::new(&config, "evil.com", true);
4446
4447 assert!(handler.is_empty());
4448 }
4449
4450 #[test]
4451 fn passthrough_host_does_not_allow_other_disallowed_placeholders() {
4452 let mut passthrough = make_secret("$PASSTHROUGH", "real-secret-a", "api.openai.com");
4453 passthrough.passthrough_hosts = vec![HostPattern::Exact("api.anthropic.com".into())];
4454 let blocked = make_secret("$BLOCKED", "real-secret-b", "api.github.com");
4455 let config = make_config(vec![passthrough, blocked]);
4456 let mut handler = SecretsHandler::new(&config, "api.anthropic.com", true);
4457
4458 let input = b"GET / HTTP/1.1\r\nX-A: $PASSTHROUGH\r\nX-B: $BLOCKED\r\n\r\n";
4459 assert_eq!(
4460 handler.substitute(input).unwrap_err(),
4461 SecretViolationAction::Block
4462 );
4463 }
4464
4465 #[test]
4466 fn per_secret_passthrough_blocks_for_non_matching_host() {
4467 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4468 secret.passthrough_hosts = vec![HostPattern::Exact("api.anthropic.com".into())];
4469 let config = make_config(vec![secret]);
4470 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4471
4472 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4473 assert_eq!(
4474 handler.substitute(input).unwrap_err(),
4475 SecretViolationAction::Block
4476 );
4477 }
4478
4479 #[test]
4480 fn passthrough_blocks_for_non_matching_host() {
4481 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4482 secret.passthrough_hosts = vec![HostPattern::Exact("api.anthropic.com".into())];
4483 let mut config = make_config(vec![secret]);
4484 config.violation_action = SecretViolationAction::BlockAndLog;
4485 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4486
4487 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4488 assert_eq!(
4489 handler.substitute(input).unwrap_err(),
4490 SecretViolationAction::BlockAndLog
4491 );
4492 }
4493
4494 #[test]
4495 fn global_block_and_terminate_marks_violation_as_terminating() {
4496 let mut config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4497 config.violation_action = SecretViolationAction::BlockAndTerminate;
4498 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4499
4500 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4501 assert_eq!(
4502 handler.substitute(input).unwrap_err(),
4503 SecretViolationAction::BlockAndTerminate
4504 );
4505 }
4506
4507 #[test]
4508 fn per_secret_block_and_terminate_marks_violation_as_terminating() {
4509 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4510 secret.violation_action = Some(SecretViolationAction::BlockAndTerminate);
4511 let config = make_config(vec![secret]);
4512 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4513
4514 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
4515 assert_eq!(
4516 handler.substitute(input).unwrap_err(),
4517 SecretViolationAction::BlockAndTerminate
4518 );
4519 }
4520
4521 #[test]
4522 fn disabled_body_substitution_preserves_placeholder_only_for_verified_allowed_host() {
4523 let body = b"{\"key\":\"$KEY\"}";
4524 for action in [
4525 SecretViolationAction::Block,
4526 SecretViolationAction::BlockAndLog,
4527 SecretViolationAction::BlockAndTerminate,
4528 ] {
4529 for host in ["api.example.com", "denied.example"] {
4530 for pinned in [false, true] {
4531 for framing in ["fixed", "chunked", "http2"] {
4532 let ip = Ipv4Addr::new(203, 0, 113, 10);
4533 let shared = SharedState::new(16);
4534 if pinned {
4535 cache_host(&shared, host, ip);
4536 }
4537 let mut secret = make_secret("$KEY", "real-secret", "api.example.com");
4538 secret.violation_action = Some(action.clone());
4539 let config = make_config(vec![secret]);
4540 let mut handler = SecretsHandler::new_tls_intercepted(
4541 &config,
4542 host,
4543 IpAddr::V4(ip),
4544 &shared,
4545 );
4546 let request = if framing == "http2" {
4547 h2_request_with_data(
4548 &[
4549 (b":method", b"POST"),
4550 (b":scheme", b"https"),
4551 (b":authority", host.as_bytes()),
4552 (b":path", b"/"),
4553 ],
4554 body,
4555 )
4556 } else if framing == "chunked" {
4557 [
4558 format!(
4559 "POST / HTTP/1.1\r\nHost: {host}\r\nTransfer-Encoding: chunked\r\n\r\n{:x}\r\n",
4560 body.len()
4561 )
4562 .as_bytes(),
4563 body,
4564 b"\r\n0\r\n\r\n",
4565 ]
4566 .concat()
4567 } else {
4568 [
4569 format!(
4570 "POST / HTTP/1.1\r\nHost: {host}\r\nContent-Length: {}\r\n\r\n",
4571 body.len()
4572 )
4573 .as_bytes(),
4574 body,
4575 ]
4576 .concat()
4577 };
4578 let result = handler.substitute(&request);
4579 if pinned && host == "api.example.com" {
4580 let output = result.unwrap();
4581 if framing == "http2" {
4582 assert!(output.ends_with(body));
4583 assert!(!contains_bytes(&output, b"real-secret"));
4584 } else {
4585 assert_eq!(output.as_ref(), request);
4586 }
4587 } else {
4588 assert_eq!(result.unwrap_err(), action);
4589 }
4590 }
4591 }
4592 }
4593 }
4594 }
4595
4596 #[test]
4597 #[allow(deprecated)] fn placeholder_permission_keeps_substitution_and_blocking_independent() {
4599 for legacy in [None, Some(false), Some(true)] {
4600 let secret = crate::config::builder::SecretBuilder::new()
4601 .env("API_KEY")
4602 .value("real-secret")
4603 .placeholder("$KEY")
4604 .allow("api.openai.com");
4605 let secret = match legacy {
4606 Some(true) => secret.allow_passthrough_for("placeholder.example"),
4607 Some(false) => secret.allow_placeholder_for("placeholder.example"),
4608 None => secret,
4609 };
4610 let config = make_config(vec![secret.build()]);
4611 let input = b"POST / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\nContent-Length: 15\r\n\r\n{\"key\": \"$KEY\"}";
4612 let mut allowed = SecretsHandler::new(&config, "api.openai.com", true);
4613 let output =
4614 String::from_utf8(allowed.substitute(input).unwrap().into_owned()).unwrap();
4615 assert!(output.contains("Authorization: Bearer real-secret"));
4616 assert!(output.contains("{\"key\": \"$KEY\"}"));
4617 let mut placeholder_host = SecretsHandler::new(&config, "placeholder.example", true);
4618 if legacy.is_some() {
4619 assert_eq!(placeholder_host.substitute(input).unwrap().as_ref(), input);
4620 } else {
4621 assert_eq!(
4622 placeholder_host.substitute(input).unwrap_err(),
4623 SecretViolationAction::Block
4624 );
4625 }
4626 let mut forbidden = SecretsHandler::new(&config, "evil.example.com", true);
4627 assert_eq!(
4628 forbidden.substitute(input).unwrap_err(),
4629 SecretViolationAction::Block
4630 );
4631 }
4632 }
4633
4634 #[test]
4635 fn body_injection_when_enabled() {
4636 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4637 secret.substitution.body = true;
4638 let config = make_config(vec![secret]);
4639 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4640
4641 let input = b"POST / HTTP/1.1\r\nContent-Length: 15\r\n\r\n{\"key\": \"$KEY\"}";
4642 let output = handler.substitute(input).unwrap();
4643 assert_eq!(
4644 String::from_utf8(output.into_owned()).unwrap(),
4645 "POST / HTTP/1.1\r\nContent-Length: 22\r\n\r\n{\"key\": \"real-secret\"}"
4646 );
4647 }
4648
4649 #[test]
4650 fn body_injection_updates_content_length() {
4651 let mut secret = make_secret("$KEY", "a]longer]secret]value", "api.openai.com");
4652 secret.substitution.body = true;
4653 let config = make_config(vec![secret]);
4654 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4655
4656 let body = "{\"key\": \"$KEY\"}";
4657 let input = format!(
4658 "POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n{}",
4659 body.len(),
4660 body
4661 );
4662 let output = handler.substitute(input.as_bytes()).unwrap();
4663 let result = String::from_utf8(output.into_owned()).unwrap();
4664
4665 let expected_body = "{\"key\": \"a]longer]secret]value\"}";
4666 assert!(result.contains(expected_body));
4667 assert!(result.contains(&format!("Content-Length: {}", expected_body.len())));
4668 }
4669
4670 #[test]
4671 fn body_injection_buffers_until_content_length_complete() {
4672 let mut secret = make_secret("$KEY", "longer-secret", "api.openai.com");
4673 secret.substitution.body = true;
4674 let config = make_config(vec![secret]);
4675 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4676
4677 let body = b"{\"key\":\"$KEY\"}";
4678 let mut chunk1 = format!(
4679 "POST / HTTP/1.1\r\nHost: api.openai.com\r\nContent-Length: {}\r\n\r\n",
4680 body.len()
4681 )
4682 .into_bytes();
4683 chunk1.extend_from_slice(&body[..5]);
4684
4685 let out1 = handler.substitute(&chunk1).unwrap();
4686 assert!(out1.is_empty());
4687
4688 let out2 = handler.substitute(&body[5..]).unwrap();
4689 let result = String::from_utf8(out2.into_owned()).unwrap();
4690 let expected_body = "{\"key\":\"longer-secret\"}";
4691 assert!(result.contains(expected_body));
4692 assert!(result.contains(&format!("Content-Length: {}", expected_body.len())));
4693 }
4694
4695 #[test]
4696 fn body_injection_blocks_content_length_over_buffer_limit() {
4697 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4698 secret.substitution.body = true;
4699 let config = make_config(vec![secret]);
4700 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4701
4702 let input = format!(
4703 "POST / HTTP/1.1\r\nHost: api.openai.com\r\nContent-Length: {}\r\n\r\n",
4704 MAX_HTTP_BODY_BUFFER_BYTES + 1
4705 );
4706
4707 assert_eq!(
4708 handler.substitute(input.as_bytes()).unwrap_err(),
4709 SecretViolationAction::Block
4710 );
4711 }
4712
4713 #[test]
4714 fn invalid_content_length_is_blocked() {
4715 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4716 secret.substitution.body = true;
4717 let config = make_config(vec![secret]);
4718 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4719
4720 let input =
4721 b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nContent-Length: nope\r\n\r\nxx$KEYyy";
4722
4723 assert_eq!(
4724 handler.substitute(input).unwrap_err(),
4725 SecretViolationAction::Block
4726 );
4727 }
4728
4729 #[test]
4730 fn conflicting_content_lengths_are_blocked() {
4731 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4732 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4733
4734 let input = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nContent-Length: 8\r\nContent-Length: 9\r\n\r\nxx$KEYyy";
4735
4736 assert_eq!(
4737 handler.substitute(input).unwrap_err(),
4738 SecretViolationAction::Block
4739 );
4740 }
4741
4742 #[test]
4743 fn body_injection_no_content_length_header() {
4744 let mut secret = make_secret("$KEY", "longer-secret", "api.openai.com");
4745 secret.substitution.body = true;
4746 let config = make_config(vec![secret]);
4747 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4748
4749 let input =
4752 b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nF\r\n{\"key\": \"$KEY\"}\r\n0\r\n\r\n";
4753 let output = handler.substitute(input).unwrap();
4754 let result = String::from_utf8(output.into_owned()).unwrap();
4755 assert!(!result.contains("$KEY"));
4756 assert!(result.contains("longer-secret"));
4757 assert!(!result.contains("Content-Length"));
4758 }
4759
4760 #[test]
4761 fn chunked_body_injection_rewrites_split_placeholder_across_chunks() {
4762 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4763 secret.substitution.body = true;
4764 let config = make_config(vec![secret]);
4765 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4766
4767 let input = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nxx$K\r\n2\r\nEY\r\n0\r\n\r\n";
4768 let output = handler.substitute(input).unwrap().into_owned();
4769 let (_, body) = split_http_body(&output);
4770 let (decoded, trailers, consumed) = decode_chunked_payload(body);
4771
4772 assert_eq!(decoded, b"xxreal-secret");
4773 assert_eq!(trailers, b"\r\n");
4774 assert_eq!(consumed, body.len());
4775 }
4776
4777 #[test]
4778 fn chunked_body_injection_rewrites_placeholder_split_across_tls_reads() {
4779 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4780 secret.substitution.body = true;
4781 let config = make_config(vec![secret]);
4782 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4783
4784 let chunk1 = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nxx$K\r\n";
4785 let chunk2 = b"2\r\nEY\r\n0\r\n\r\n";
4786
4787 let mut output = handler.substitute(chunk1).unwrap().into_owned();
4788 output.extend_from_slice(handler.substitute(chunk2).unwrap().as_ref());
4789 let (_, body) = split_http_body(&output);
4790 let (decoded, trailers, consumed) = decode_chunked_payload(body);
4791
4792 assert_eq!(decoded, b"xxreal-secret");
4793 assert_eq!(trailers, b"\r\n");
4794 assert_eq!(consumed, body.len());
4795 }
4796
4797 #[test]
4798 fn chunked_body_injection_preserves_trailers_and_recurses_to_next_request() {
4799 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4800 secret.substitution.body = true;
4801 let config = make_config(vec![secret]);
4802 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4803
4804 let mut input = b"POST /a HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: chunked\r\n\r\n4\r\n$KEY\r\n0\r\nX-Trailer: yes\r\n\r\n".to_vec();
4805 input.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: api.openai.com\r\nAuth: $KEY\r\n\r\n");
4806
4807 let output = handler.substitute(&input).unwrap().into_owned();
4808 let (_, body_and_next) = split_http_body(&output);
4809 let (decoded, trailers, consumed) = decode_chunked_payload(body_and_next);
4810 let next_request = &body_and_next[consumed..];
4811
4812 assert_eq!(decoded, b"real-secret");
4813 assert_eq!(trailers, b"X-Trailer: yes\r\n\r\n");
4814 assert_eq!(
4815 next_request,
4816 b"GET /b HTTP/1.1\r\nHost: api.openai.com\r\nAuth: real-secret\r\n\r\n"
4817 );
4818 }
4819
4820 #[test]
4821 fn chunked_body_injection_blocks_content_encoded_placeholder() {
4822 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4823 secret.substitution.body = true;
4824 let config = make_config(vec![secret]);
4825 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4826
4827 let input = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: chunked\r\nContent-Encoding: gzip\r\n\r\n4\r\n$KEY\r\n0\r\n\r\n";
4828
4829 assert_eq!(
4830 handler.substitute(input).unwrap_err(),
4831 SecretViolationAction::Block
4832 );
4833 }
4834
4835 #[test]
4836 fn unsupported_transfer_encoding_chain_is_blocked() {
4837 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4838 secret.substitution.body = true;
4839 let config = make_config(vec![secret]);
4840 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4841
4842 let input = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: gzip, chunked\r\n\r\n4\r\n$KEY\r\n0\r\n\r\n";
4843
4844 assert_eq!(
4845 handler.substitute(input).unwrap_err(),
4846 SecretViolationAction::Block
4847 );
4848 }
4849
4850 #[test]
4851 fn transfer_encoding_with_content_length_is_blocked() {
4852 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4853 secret.substitution.body = true;
4854 let config = make_config(vec![secret]);
4855 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4856
4857 let input = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: chunked\r\nContent-Length: 4\r\n\r\n4\r\n$KEY\r\n0\r\n\r\n";
4858
4859 assert_eq!(
4860 handler.substitute(input).unwrap_err(),
4861 SecretViolationAction::Block
4862 );
4863 }
4864
4865 #[test]
4866 fn split_chunked_body_payload_blocks_for_wrong_host() {
4867 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4868 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4869
4870 let input = b"POST / HTTP/1.1\r\nHost: evil.com\r\nTransfer-Encoding: chunked\r\n\r\n2\r\n$K\r\n2\r\nEY\r\n0\r\n\r\n";
4871
4872 assert_eq!(
4873 handler.substitute(input).unwrap_err(),
4874 SecretViolationAction::Block
4875 );
4876 }
4877
4878 #[test]
4879 fn split_chunked_trailer_blocks_for_wrong_host() {
4880 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4881 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4882
4883 let first = b"POST / HTTP/1.1\r\nHost: evil.com\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nx\r\n0\r\nX-Token: $K";
4884 assert_eq!(handler.substitute(first).unwrap().as_ref(), first);
4885 assert_eq!(
4886 handler.substitute(b"EY\r\n\r\n").unwrap_err(),
4887 SecretViolationAction::Block
4888 );
4889 }
4890
4891 #[test]
4892 fn split_chunked_trailer_preserves_placeholder_on_allowed_host() {
4893 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4894 secret.substitution.body = true;
4895 let config = make_config(vec![secret]);
4896 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4897
4898 let first = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nx\r\n0\r\nX-Token: $K";
4902 let output = handler.substitute(first).unwrap();
4903 assert!(!output.as_ref().ends_with(b"$K"));
4904 assert_eq!(
4905 handler.substitute(b"EY\r\n\r\n").unwrap().as_ref(),
4906 b"X-Token: $KEY\r\n\r\n"
4907 );
4908 }
4909
4910 #[test]
4911 fn split_chunk_extension_blocks_for_wrong_host() {
4912 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4913 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4914
4915 let first =
4916 b"POST / HTTP/1.1\r\nHost: evil.com\r\nTransfer-Encoding: chunked\r\n\r\n1;token=$K";
4917 assert_eq!(handler.substitute(first).unwrap().as_ref(), first);
4918 assert_eq!(
4919 handler.substitute(b"EY\r\nx\r\n0\r\n\r\n").unwrap_err(),
4920 SecretViolationAction::Block
4921 );
4922 }
4923
4924 #[test]
4925 fn split_chunk_extension_blocks_during_body_rewrite() {
4926 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4927 secret.substitution.body = true;
4928 let config = make_config(vec![
4929 secret,
4930 make_secret("$BLOCKED", "other-secret", "other.example"),
4931 ]);
4932 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4933
4934 let first = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: chunked\r\n\r\n1;token=$B";
4935 let output = handler.substitute(first).unwrap();
4936 assert!(!output.as_ref().ends_with(b"$B"));
4937 assert_eq!(
4938 handler.substitute(b"LOCKED\r\nx\r\n0\r\n\r\n").unwrap_err(),
4939 SecretViolationAction::Block
4940 );
4941 }
4942
4943 #[test]
4944 fn chunked_passthrough_allows_split_metadata_placeholders() {
4945 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4946 secret.passthrough_hosts = vec![HostPattern::Exact("evil.com".into())];
4947 let config = make_config(vec![secret]);
4948 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4949
4950 let fragments: [&[u8]; 3] = [
4951 b"POST / HTTP/1.1\r\nHost: evil.com\r\nTransfer-Encoding: chunked\r\n\r\n1;token=$K",
4952 b"EY\r\nx\r\n0\r\nX-Token: $K",
4953 b"EY\r\n\r\n",
4954 ];
4955 for fragment in fragments {
4956 assert_eq!(handler.substitute(fragment).unwrap().as_ref(), fragment);
4957 }
4958 }
4959
4960 #[test]
4961 fn chunked_rewrite_substitutes_body_and_preserves_passthrough_trailer() {
4962 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4963 secret.substitution.body = true;
4964 secret.passthrough_hosts = vec![HostPattern::Exact("api.openai.com".into())];
4965 let config = make_config(vec![secret]);
4966 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
4967
4968 let input = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nTransfer-Encoding: chunked\r\n\r\n4\r\n$KEY\r\n0\r\nX-Token: $KEY\r\n\r\n";
4969 let output = handler.substitute(input).unwrap().into_owned();
4970 let (_, body) = split_http_body(&output);
4971 let (decoded, trailers, consumed) = decode_chunked_payload(body);
4972
4973 assert_eq!(decoded, b"real-secret");
4974 assert_eq!(trailers, b"X-Token: $KEY\r\n\r\n");
4975 assert_eq!(consumed, body.len());
4976 }
4977
4978 #[test]
4979 fn chunked_detection_does_not_join_distinct_protocol_locations() {
4980 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4981 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4982
4983 let input = b"POST / HTTP/1.1\r\nHost: evil.com\r\nTransfer-Encoding: chunked\r\n\r\n2;note=$K\r\nEY\r\n2\r\n$K\r\n0\r\nEY: yes\r\n\r\n";
4987 assert_eq!(handler.substitute(input).unwrap().as_ref(), input);
4988 }
4989
4990 #[test]
4991 fn basic_auth_placeholder_in_chunked_trailer_is_blocked() {
4992 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4993 let mut handler = SecretsHandler::new(&config, "evil.com", true);
4994
4995 let input = b"POST / HTTP/1.1\r\nHost: evil.com\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nAuthorization: Basic YWRtaW46JEtFWQ==\r\n\r\n";
4998 assert_eq!(
4999 handler.substitute(input).unwrap_err(),
5000 SecretViolationAction::Block
5001 );
5002 }
5003
5004 #[test]
5005 fn chunked_trailer_violation_keeps_original_request_context() {
5006 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5007 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5008 handler.http1_request_summary = Some(http1_request_summary(
5009 "POST /upload?ignored=yes HTTP/1.1\r\nHost: evil.com\r\n\r\n",
5010 ));
5011
5012 let trailer = b"Authorization: Basic YWRtaW46JEtFWQ==\r\n";
5013 let report = handler
5014 .detect_http1_fragment_blocking_action(
5015 &[],
5016 trailer,
5017 std::str::from_utf8(trailer).unwrap(),
5018 RequestLocation::Trailer,
5019 )
5020 .unwrap();
5021
5022 assert_eq!(report.location, RequestLocation::Trailer);
5023 assert!(matches!(
5024 report.match_form,
5025 PlaceholderMatchForm::BasicAuthDecoded
5026 ));
5027 assert_eq!(report.method.as_deref(), Some("POST"));
5028 assert_eq!(report.path.as_deref(), Some("/upload"));
5029 assert_eq!(report.host.as_deref(), Some("evil.com"));
5030 }
5031
5032 #[test]
5033 fn oversized_secret_placeholder_is_rejected() {
5034 let placeholder = "x".repeat(MAX_SECRET_PLACEHOLDER_BYTES + 1);
5035 let config = make_config(vec![make_secret(
5036 &placeholder,
5037 "real-secret",
5038 "api.openai.com",
5039 )]);
5040 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5041
5042 assert_eq!(
5043 handler.substitute(b"GET / HTTP/1.1\r\n\r\n").unwrap_err(),
5044 SecretViolationAction::Block
5045 );
5046 }
5047
5048 #[test]
5049 fn header_only_substitution_preserves_content_length() {
5050 let config = make_config(vec![make_secret("$KEY", "longer-value", "api.openai.com")]);
5051 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5052
5053 let input =
5054 b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\nContent-Length: 5\r\n\r\nhello";
5055 let output = handler.substitute(input).unwrap();
5056 let result = String::from_utf8(output.into_owned()).unwrap();
5057 assert!(result.contains("Content-Length: 5"));
5059 assert!(result.ends_with("hello"));
5060 }
5061
5062 #[test]
5063 fn eligible_secret_preserves_binary_body_without_placeholder() {
5064 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5065 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5066
5067 let body = vec![0x1f, 0x8b, 0x08, 0x00, 0xff, 0x00, 0x80, 0xfe];
5068 let mut input = format!(
5069 "POST /git-upload-pack HTTP/1.1\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\n\r\n",
5070 body.len()
5071 )
5072 .into_bytes();
5073 input.extend_from_slice(&body);
5074
5075 let output = handler.substitute(&input).unwrap();
5076 assert_eq!(&*output, input.as_slice());
5077 }
5078
5079 #[test]
5080 fn body_injection_blocks_content_encoded_placeholder() {
5081 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5082 secret.substitution.body = true;
5083 let config = make_config(vec![secret]);
5084 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5085
5086 let body = b"compressed-looking-$KEY-bytes";
5087 let mut input = format!(
5088 "POST /git-upload-pack HTTP/1.1\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\n\r\n",
5089 body.len()
5090 )
5091 .into_bytes();
5092 input.extend_from_slice(body);
5093
5094 assert_eq!(
5095 handler.substitute(&input).unwrap_err(),
5096 SecretViolationAction::Block
5097 );
5098 }
5099
5100 #[test]
5101 fn body_injection_blocks_split_content_encoded_placeholder() {
5102 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5103 secret.substitution.body = true;
5104 let config = make_config(vec![secret]);
5105 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5106
5107 let first = b"POST /git-upload-pack HTTP/1.1\r\nContent-Encoding: gzip\r\nContent-Length: 4\r\n\r\n$K";
5108
5109 let output = handler.substitute(first).unwrap();
5110 assert_eq!(&*output, first.as_slice());
5111 assert_eq!(
5112 handler.substitute(b"EY").unwrap_err(),
5113 SecretViolationAction::Block
5114 );
5115 }
5116
5117 #[test]
5118 fn eligible_secret_preserves_binary_chunk_without_placeholder() {
5119 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5120 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5121
5122 let input = [0x1f, 0x8b, 0x08, 0x00, 0xff, 0x00, 0x80, 0xfe];
5123 let output = handler.substitute(&input).unwrap();
5124 assert_eq!(&*output, input.as_slice());
5125 }
5126
5127 #[test]
5128 fn body_injection_preserves_non_utf8_bytes() {
5129 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5130 secret.substitution.body = true;
5131 let config = make_config(vec![secret]);
5132 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5133
5134 let body = [0xff, b'$', b'K', b'E', b'Y', 0xfe];
5135 let mut input =
5136 format!("POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n", body.len()).into_bytes();
5137 input.extend_from_slice(&body);
5138
5139 let output = handler.substitute(&input).unwrap().into_owned();
5140 let expected_body = [b"\xffreal-secret".as_slice(), &[0xfe]].concat();
5141 let expected = [
5142 format!(
5143 "POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n",
5144 expected_body.len()
5145 )
5146 .as_bytes(),
5147 expected_body.as_slice(),
5148 ]
5149 .concat();
5150
5151 assert_eq!(output, expected);
5152 }
5153
5154 #[test]
5155 fn no_secrets_passthrough() {
5156 let config = make_config(vec![]);
5157 let mut handler = SecretsHandler::new(&config, "anything.com", true);
5158
5159 let input = b"GET / HTTP/1.1\r\n\r\n";
5160 let output = handler.substitute(input).unwrap();
5161 assert_eq!(&*output, input);
5162 }
5163
5164 #[test]
5165 fn require_tls_identity_blocks_on_non_intercepted() {
5166 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5167 let mut handler = SecretsHandler::new(&config, "api.openai.com", false);
5169
5170 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
5171 assert_eq!(
5172 handler.substitute(input).unwrap_err(),
5173 SecretViolationAction::Block
5174 );
5175 }
5176
5177 #[test]
5178 fn new_plain_http_blocks_require_tls_identity_secrets() {
5179 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5181 let shared = SharedState::new(4);
5182 let ip = Ipv4Addr::new(1, 2, 3, 4);
5183 cache_host(&shared, "api.openai.com", ip);
5184 let mut handler =
5185 SecretsHandler::new_plain_http(&config, "api.openai.com", IpAddr::V4(ip), &shared);
5186
5187 let input = b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nContent-Length: 4\r\n\r\n$KEY";
5188 assert_eq!(
5189 handler.substitute(input).unwrap_err(),
5190 SecretViolationAction::Block
5191 );
5192 }
5193
5194 #[test]
5195 fn new_plain_http_substitutes_when_tls_identity_not_required() {
5196 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5198 secret.require_tls_identity = false;
5199 let config = make_config(vec![secret]);
5200 let shared = SharedState::new(4);
5201 let ip = Ipv4Addr::new(1, 2, 3, 4);
5202 cache_host(&shared, "api.openai.com", ip);
5203 let mut handler =
5204 SecretsHandler::new_plain_http(&config, "api.openai.com", IpAddr::V4(ip), &shared);
5205
5206 let input = b"POST / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\nHost: api.openai.com\r\nContent-Length: 4\r\n\r\n$KEY";
5207 let output = handler.substitute(input).unwrap();
5208 assert_eq!(output.as_ref(), b"POST / HTTP/1.1\r\nAuthorization: Bearer real-secret\r\nHost: api.openai.com\r\nContent-Length: 4\r\n\r\n$KEY");
5209 }
5210
5211 #[test]
5212 fn new_plain_http_invalid_host_blocks_host_bound_secret() {
5213 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5216 secret.require_tls_identity = false;
5217 let config = make_config(vec![secret]);
5218 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5219
5220 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
5221 assert!(handler.substitute(input).is_err());
5223 }
5224
5225 #[test]
5226 fn new_plain_http_invalid_host_substitutes_when_all_secrets_any() {
5227 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5230 secret.require_tls_identity = false;
5231 secret.allowed_hosts = vec![HostPattern::Any];
5232 let config = make_config(vec![secret]);
5233 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5234
5235 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
5236 let output = handler.substitute(input).unwrap();
5237 assert!(
5238 String::from_utf8(output.into_owned())
5239 .unwrap()
5240 .contains("real-secret")
5241 );
5242 }
5243
5244 #[test]
5245 fn new_plain_http_invalid_host_blocks_any_secret_when_mixed() {
5246 let mut any_secret = make_secret("$ANY", "any-value", "api.openai.com");
5249 any_secret.require_tls_identity = false;
5250 any_secret.allowed_hosts = vec![HostPattern::Any];
5251 let mut bound_secret = make_secret("$BOUND", "bound-value", "api.openai.com");
5252 bound_secret.require_tls_identity = false;
5253 let config = make_config(vec![any_secret, bound_secret]);
5254 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5255
5256 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $ANY\r\n\r\n";
5258 assert!(handler.substitute(input).is_err());
5259 }
5260
5261 #[test]
5262 fn opaque_prefix_never_receives_secret_substitution() {
5263 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5264 secret.require_tls_identity = false;
5265 secret.allowed_hosts = vec![HostPattern::Any];
5266 let config = make_config(vec![secret]);
5267 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5268 let input = b"BINARY3 v1\0opaque\r\nAuthorization: $KEY\r\n\r\n";
5269
5270 assert_eq!(handler.substitute(input).unwrap().as_ref(), input);
5271 }
5272
5273 #[test]
5274 fn opaque_prefix_blocks_basic_auth_encoded_placeholder() {
5275 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5276 secret.require_tls_identity = false;
5277 let config = make_config(vec![secret]);
5278 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5279 let input = b"BINARY3 v1\0\r\nAuthorization: Basic dXNlcjokS0VZ\r\n\r\n";
5280
5281 assert_eq!(handler.substitute(input), Err(SecretViolationAction::Block));
5282 }
5283
5284 #[test]
5285 fn opaque_prefix_blocks_basic_auth_split_after_long_prefix() {
5286 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5287 secret.require_tls_identity = false;
5288 let config = make_config(vec![secret]);
5289 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5290 let decoded_prefix = format!("user:{}", "x".repeat(97));
5291 assert_eq!(decoded_prefix.len() % 3, 0);
5292 let encoded_prefix = BASE64.encode(&decoded_prefix);
5293 let encoded = BASE64.encode(format!("{decoded_prefix}$KEY"));
5294 let first = format!("BINARY3 v1\0\r\nAuthorization: Basic {encoded_prefix}");
5295
5296 assert_eq!(
5297 handler.substitute(first.as_bytes()).unwrap(),
5298 first.as_bytes()
5299 );
5300 assert_eq!(
5301 handler.substitute(format!("{}\r\n\r\n", &encoded[encoded_prefix.len()..]).as_bytes()),
5302 Err(SecretViolationAction::Block)
5303 );
5304 }
5305
5306 #[test]
5307 fn opaque_prefix_forwards_oversized_authorization_like_data() {
5308 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5309 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5310 let mut input = b"BINARY3 v1\0\r\naUtHoRiZaTiOn: ".to_vec();
5311 input.resize(input.len() + MAX_HTTP_HEADER_BYTES + 1, b'x');
5312
5313 assert_eq!(handler.substitute(&input).unwrap(), input.as_slice());
5314 }
5315
5316 #[test]
5317 fn opaque_prefix_blocks_oversized_basic_auth_split_placeholder() {
5318 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5319 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5320 let decoded_prefix = vec![b'x'; 3 * (MAX_HTTP_HEADER_BYTES / 4 + 1)];
5321 let encoded_prefix = BASE64.encode(&decoded_prefix);
5322 let encoded = BASE64.encode([decoded_prefix.as_slice(), b"$KEY"].concat());
5323 let first = format!("BINARY3 v1\0\r\nAuthorization: Basic {encoded_prefix}");
5324
5325 assert_eq!(
5326 handler.substitute(first.as_bytes()).unwrap(),
5327 first.as_bytes()
5328 );
5329 assert_eq!(
5330 handler.substitute(format!("{}\r\n", &encoded[encoded_prefix.len()..]).as_bytes()),
5331 Err(SecretViolationAction::Block)
5332 );
5333 }
5334
5335 #[test]
5336 fn opaque_prefix_blocks_basic_auth_with_split_header_name() {
5337 let config = make_config(vec![make_secret("$", "real-secret", "api.openai.com")]);
5338 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5339 let first = b"BINARY3 v1\0opaque\r\nAuthorizatio";
5340
5341 assert_eq!(handler.substitute(first).unwrap(), &first[..]);
5342 assert_eq!(
5343 handler.substitute(b"n: Basic dXNlcjok\r\n\r\n"),
5344 Err(SecretViolationAction::Block)
5345 );
5346 }
5347
5348 #[test]
5349 fn opaque_prefix_blocks_placeholder_split_across_writes() {
5350 let mut secret = make_secret("$MSB_KEY", "real-secret", "api.openai.com");
5351 secret.require_tls_identity = false;
5352 let config = make_config(vec![secret]);
5353 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5354
5355 assert_eq!(
5356 handler.substitute(b"BINARY3 v1\0opaque $MS").unwrap(),
5357 &b"BINARY3 v1\0opaque $MS"[..]
5358 );
5359 assert_eq!(
5360 handler.substitute(b"B_KEY\0request"),
5361 Err(SecretViolationAction::Block)
5362 );
5363 }
5364
5365 #[test]
5366 fn opaque_prefix_keeps_later_ascii_writes_opaque() {
5367 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5368 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5369
5370 assert_eq!(
5371 handler.substitute(b"BINARY3 v1\0opaque").unwrap(),
5372 &b"BINARY3 v1\0opaque"[..]
5373 );
5374 assert_eq!(handler.substitute(b"PING").unwrap(), &b"PING"[..]);
5375 }
5376
5377 #[test]
5378 fn tab_delimited_non_http_prefix_is_not_buffered() {
5379 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5380 let mut handler = SecretsHandler::new_plain_http_invalid_host(&config);
5381
5382 assert_eq!(
5383 handler.substitute(b"PING\tpayload").unwrap(),
5384 &b"PING\tpayload"[..]
5385 );
5386 }
5387
5388 #[test]
5389 fn opaque_prefix_uses_connection_policy() {
5390 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
5391 let mut handler = plain_http_policy_handler(&config);
5392
5393 assert_eq!(
5394 handler.substitute(b"AMQP\0\0\x09\x01").unwrap(),
5395 &b"AMQP\0\0\x09\x01"[..]
5396 );
5397 assert_eq!(
5398 handler.substitute(b"PING HTTP/1.1").unwrap(),
5399 &b"PING HTTP/1.1"[..]
5400 );
5401 assert_eq!(
5402 handler.substitute(b" $KEY"),
5403 Err(SecretViolationAction::Block)
5404 );
5405 }
5406
5407 #[test]
5408 fn http_shaped_binary_control_is_blocked_when_authority_is_required() {
5409 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
5410 let mut handler = plain_http_policy_handler(&config);
5411
5412 assert_eq!(
5413 handler.substitute(b"GET\0 / HTTP/1.1\r\nHost: b.example\r\n\r\n"),
5414 Err(SecretViolationAction::Block)
5415 );
5416 }
5417
5418 #[test]
5419 fn basic_auth_decodes_substitutes_and_reencodes_credentials() {
5420 let mut user = make_secret("$MSB_USER", "alice", "api.openai.com");
5421 user.env_var = "USER".into();
5422 user.substitution = basic_auth_only();
5423 let mut password = make_secret("$MSB_PASSWORD", "s3cr3t", "api.openai.com");
5424 password.env_var = "PASSWORD".into();
5425 password.substitution = basic_auth_only();
5426 let config = make_config(vec![user, password]);
5427 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5428
5429 let encoded = BASE64.encode(b"$MSB_USER:$MSB_PASSWORD");
5430 let input = format!("GET / HTTP/1.1\r\nAuthorization: Basic {encoded}\r\n\r\n");
5431 let output = handler.substitute(input.as_bytes()).unwrap();
5432 let result = String::from_utf8(output.into_owned()).unwrap();
5433
5434 assert!(result.contains(&format!(
5435 "Authorization: Basic {}",
5436 BASE64.encode(b"alice:s3cr3t")
5437 )));
5438 assert!(!result.contains("$MSB_USER"));
5439 assert!(!result.contains("$MSB_PASSWORD"));
5440 }
5441
5442 #[test]
5443 fn basic_auth_encoded_placeholder_is_blocked_for_wrong_host() {
5444 let mut secret = make_secret("$MSB_PASSWORD", "s3cr3t", "api.openai.com");
5445 secret.substitution = basic_auth_only();
5446 let config = make_config(vec![secret]);
5447 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5448
5449 let encoded = BASE64.encode(b"user:$MSB_PASSWORD");
5450 let input = format!("GET / HTTP/1.1\r\nAuthorization: Basic {encoded}\r\n\r\n");
5451
5452 assert_eq!(
5453 handler.substitute(input.as_bytes()).unwrap_err(),
5454 SecretViolationAction::Block
5455 );
5456 }
5457
5458 #[test]
5459 fn basic_auth_encoded_placeholder_is_not_replaced_when_scope_disabled() {
5460 let mut secret = make_secret("$MSB_PASSWORD", "s3cr3t", "api.openai.com");
5461 secret.substitution = SecretSubstitution {
5462 headers: false,
5463 header_fields: Vec::new(),
5464 query: false,
5465 body: true,
5466 };
5467 let config = make_config(vec![secret]);
5468 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5469
5470 let encoded = BASE64.encode(b"user:$MSB_PASSWORD");
5471 let input = format!("GET / HTTP/1.1\r\nAuthorization: Basic {encoded}\r\n\r\n");
5472 assert_eq!(
5473 handler.substitute(input.as_bytes()).unwrap().as_ref(),
5474 input.as_bytes()
5475 );
5476 }
5477
5478 #[test]
5479 fn query_params_substitution() {
5480 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5481 secret.substitution = SecretSubstitution {
5482 headers: false,
5483 header_fields: Vec::new(),
5484 query: true,
5485 body: false,
5486 };
5487 let config = make_config(vec![secret]);
5488 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5489
5490 let input = b"GET /api?key=$KEY HTTP/1.1\r\nHost: api.openai.com\r\n\r\n";
5491 let output = handler.substitute(input).unwrap();
5492 let result = String::from_utf8(output.into_owned()).unwrap();
5493 assert!(result.contains("GET /api?key=real-secret HTTP/1.1"));
5495 }
5497
5498 #[test]
5499 fn query_params_do_not_substitute_path() {
5500 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
5501 secret.substitution = SecretSubstitution {
5502 headers: false,
5503 header_fields: Vec::new(),
5504 query: true,
5505 body: false,
5506 };
5507 let config = make_config(vec![secret]);
5508 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5509
5510 let input = b"GET /path/$KEY?token=$KEY HTTP/1.1\r\nHost: api.openai.com\r\n\r\n";
5511 let output = handler.substitute(input).unwrap();
5512 let result = String::from_utf8(output.into_owned()).unwrap();
5513
5514 assert!(result.contains("GET /path/$KEY?token=real-secret HTTP/1.1"));
5515 }
5516
5517 #[test]
5518 fn header_injection_does_not_substitute_request_line_query() {
5519 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5520 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
5521
5522 let input = b"GET /api?key=$KEY HTTP/1.1\r\nHost: api.openai.com\r\n\r\n";
5523 assert_eq!(handler.substitute(input).unwrap().as_ref(), input);
5524 }
5525
5526 #[test]
5527 fn url_encoded_placeholder_in_query_blocks_for_wrong_host() {
5528 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5529 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5530
5531 let input = b"GET /api?token=%24KEY HTTP/1.1\r\nHost: evil.com\r\n\r\n";
5533 assert_eq!(
5534 handler.substitute(input).unwrap_err(),
5535 SecretViolationAction::Block
5536 );
5537 }
5538
5539 #[test]
5540 fn url_encoded_placeholder_in_body_blocks_for_wrong_host() {
5541 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5542 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5543
5544 let input = b"POST / HTTP/1.1\r\nContent-Length: 13\r\n\r\nkey=%24KEY&x=1";
5545 assert_eq!(
5546 handler.substitute(input).unwrap_err(),
5547 SecretViolationAction::Block
5548 );
5549 }
5550
5551 #[test]
5552 fn json_escaped_placeholder_in_body_blocks_for_wrong_host() {
5553 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5554 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5555
5556 let input =
5558 b"POST / HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{\"k\":\"\\u0024KEY\"}";
5559 assert_eq!(
5560 handler.substitute(input).unwrap_err(),
5561 SecretViolationAction::Block
5562 );
5563 }
5564
5565 #[test]
5566 fn split_url_encoded_placeholder_blocks_for_wrong_host() {
5567 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5568 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5569
5570 let chunk1 = b"POST / HTTP/1.1\r\nHost: evil.com\r\nContent-Length: 14\r\n\r\nkey=%24K";
5571 let chunk2 = b"EY&x=1";
5572
5573 assert!(handler.substitute(chunk1).is_ok());
5574 assert_eq!(
5575 handler.substitute(chunk2).unwrap_err(),
5576 SecretViolationAction::Block
5577 );
5578 }
5579
5580 #[test]
5581 fn split_json_escaped_placeholder_blocks_for_wrong_host() {
5582 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5583 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5584
5585 let chunk1 =
5586 b"POST / HTTP/1.1\r\nHost: evil.com\r\nContent-Length: 17\r\n\r\n{\"k\":\"\\u0024K";
5587 let chunk2 = b"EY\"}";
5588
5589 assert!(handler.substitute(chunk1).is_ok());
5590 assert_eq!(
5591 handler.substitute(chunk2).unwrap_err(),
5592 SecretViolationAction::Block
5593 );
5594 }
5595
5596 #[test]
5597 fn placeholder_split_across_writes_blocks_for_wrong_host() {
5598 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5599 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5600
5601 let first = b"GET / HTTP/1.1\r\nX-Token: $K";
5603 let second = b"EY\r\nHost: evil.com\r\n\r\n";
5604
5605 assert!(handler.substitute(first).is_ok());
5607 assert_eq!(
5609 handler.substitute(second).unwrap_err(),
5610 SecretViolationAction::Block
5611 );
5612 }
5613
5614 #[test]
5615 fn split_headers_do_not_leak_header_secret_into_body() {
5616 let config = make_config(vec![make_passthrough_secret(
5617 "$KEY",
5618 "real-secret",
5619 "example.com",
5620 )]);
5621 let mut handler = SecretsHandler::new(&config, "example.com", true);
5622
5623 let chunk1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 8\r\n";
5624 let out1 = handler.substitute(chunk1).unwrap();
5625 assert!(out1.is_empty());
5626
5627 let chunk2 = b"\r\nxx$KEYyy";
5628 let out2 = handler.substitute(chunk2).unwrap();
5629 let result = String::from_utf8(out2.into_owned()).unwrap();
5630
5631 assert!(result.contains("xx$KEYyy"));
5632 assert!(!result.contains("real-secret"));
5633 }
5634
5635 #[test]
5636 fn url_decoded_contains_basic() {
5637 assert!(url_decoded_contains(b"foo%24KEYbar", b"$KEY"));
5638 assert!(!url_decoded_contains(b"fooKEYbar", b"$KEY"));
5639 assert!(url_decoded_contains(b"%2", b"%2"));
5641 }
5642
5643 #[test]
5644 fn json_escaped_contains_basic() {
5645 assert!(json_escaped_contains(b"\"\\u0024KEY\"", b"$KEY"));
5646 assert!(json_escaped_contains(
5647 b"\\u0024\\u004B\\u0045\\u0059",
5648 b"$KEY"
5649 ));
5650 assert!(!json_escaped_contains(b"KEY", b"$KEY"));
5651 }
5652
5653 #[test]
5654 fn body_in_separate_chunk_preserves_non_utf8_bytes() {
5655 let config = make_config(vec![make_passthrough_secret(
5667 "$KEY",
5668 "real-secret",
5669 "example.com",
5670 )]);
5671 let mut handler = SecretsHandler::new(&config, "example.com", true);
5672
5673 let chunk1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 13\r\n\r\n";
5675 handler.substitute(chunk1).unwrap();
5676
5677 let mut body: Vec<u8> = vec![0x00, 0x80, 0xc0, 0xff, 0xfe];
5680 body.extend_from_slice(b"$KEY");
5681 body.extend_from_slice(&[0x81, 0xc1, 0xee, 0xef]);
5682 assert_eq!(body.len(), 13);
5683
5684 let out = handler.substitute(&body).unwrap();
5685 assert_eq!(out.as_ref(), body.as_slice());
5686 }
5687
5688 #[test]
5689 fn body_split_across_two_chunks_round_trips() {
5690 let config = make_config(vec![make_passthrough_secret(
5699 "$KEY",
5700 "real-secret",
5701 "example.com",
5702 )]);
5703 let mut handler = SecretsHandler::new(&config, "example.com", true);
5704
5705 let mut body: Vec<u8> = vec![0x00, 0x80, 0xc0, 0xff, 0xfe, 0xfd, 0xfc];
5706 body.extend_from_slice(b"$KEY");
5707 body.extend_from_slice(&[0x81, 0xc1, 0xee, 0xef]);
5708 assert_eq!(body.len(), 15);
5709
5710 let mut chunk1 =
5711 b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 15\r\n\r\n".to_vec();
5712 chunk1.extend_from_slice(&body[..5]);
5713
5714 let out1 = handler.substitute(&chunk1).unwrap();
5715 let boundary = out1
5716 .windows(4)
5717 .position(|w| w == b"\r\n\r\n")
5718 .map(|p| p + 4)
5719 .unwrap();
5720 assert_eq!(&out1[boundary..], &body[..5]);
5721
5722 let out2 = handler.substitute(&body[5..]).unwrap();
5723 assert_eq!(out2.as_ref(), &body[5..]);
5724 }
5725
5726 #[test]
5727 fn framing_state_resets_after_request_completes() {
5728 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
5732 let mut handler = SecretsHandler::new(&config, "example.com", true);
5733
5734 let body: Vec<u8> = vec![0x00, 0x80, 0xc0, 0xff, 0xfe];
5735 let mut chunk1 =
5736 b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n".to_vec();
5737 chunk1.extend_from_slice(&body);
5738 handler.substitute(&chunk1).unwrap();
5739
5740 let chunk2 = b"GET /b HTTP/1.1\r\nHost: example.com\r\n\r\n";
5743 let out2 = handler.substitute(chunk2).unwrap();
5744 assert_eq!(out2.as_ref(), chunk2.as_slice());
5745 }
5746
5747 #[test]
5748 fn violation_detected_in_body_continuation_chunk() {
5749 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5753 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5754
5755 let chunk1 = b"POST /a HTTP/1.1\r\nHost: evil.com\r\nContent-Length: 16\r\n\r\n";
5756 handler.substitute(chunk1).unwrap();
5757
5758 let chunk2 = b"prefix:$KEY:suffix";
5759 assert_eq!(
5760 handler.substitute(chunk2).unwrap_err(),
5761 SecretViolationAction::Block
5762 );
5763 }
5764
5765 #[test]
5766 fn header_only_secret_preserves_placeholder_in_body_continuation_chunk() {
5767 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
5775 let mut handler = SecretsHandler::new(&config, "example.com", true);
5776
5777 let chunk1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 24\r\n\r\n";
5779 handler.substitute(chunk1).unwrap();
5780
5781 let body = b"prefix:$KEY:more-padding";
5784 assert_eq!(body.len(), 24);
5785 assert_eq!(handler.substitute(body).unwrap().as_ref(), body);
5786 }
5787
5788 #[test]
5789 fn pipelined_request_in_body_continuation_chunk_is_substituted() {
5790 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
5796 let mut handler = SecretsHandler::new(&config, "example.com", true);
5797
5798 let mut chunk1 =
5800 b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n".to_vec();
5801 chunk1.extend_from_slice(b"abcd");
5802 handler.substitute(&chunk1).unwrap();
5803
5804 let mut chunk2 = b"e".to_vec();
5807 chunk2.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
5808
5809 let out = handler.substitute(&chunk2).unwrap();
5810
5811 let mut expected = b"e".to_vec();
5812 expected.extend_from_slice(
5813 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
5814 );
5815 assert_eq!(out.as_ref(), expected.as_slice());
5816 }
5817
5818 #[test]
5819 fn pipelined_request_in_same_chunk_as_headers_is_substituted() {
5820 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
5826 let mut handler = SecretsHandler::new(&config, "example.com", true);
5827
5828 let mut chunk =
5829 b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n".to_vec();
5830 chunk.extend_from_slice(b"abcde");
5831 chunk.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
5832
5833 let out = handler.substitute(&chunk).unwrap();
5834
5835 let mut expected =
5836 b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n".to_vec();
5837 expected.extend_from_slice(b"abcde");
5838 expected.extend_from_slice(
5839 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
5840 );
5841 assert_eq!(out.as_ref(), expected.as_slice());
5842 }
5843
5844 #[test]
5845 fn three_pipelined_requests_in_one_chunk_all_substitute() {
5846 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
5850 let mut handler = SecretsHandler::new(&config, "example.com", true);
5851
5852 let r1 =
5853 b"POST /a HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\nContent-Length: 3\r\n\r\nbod";
5854 let r2 =
5855 b"PUT /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\nContent-Length: 2\r\n\r\nXY";
5856 let r3 = b"GET /c HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n";
5857 let mut chunk = Vec::new();
5858 chunk.extend_from_slice(r1);
5859 chunk.extend_from_slice(r2);
5860 chunk.extend_from_slice(r3);
5861
5862 let out = handler.substitute(&chunk).unwrap();
5863
5864 let r1_out = b"POST /a HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\nContent-Length: 3\r\n\r\nbod";
5865 let r2_out = b"PUT /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\nContent-Length: 2\r\n\r\nXY";
5866 let r3_out = b"GET /c HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n";
5867 let mut expected = Vec::new();
5868 expected.extend_from_slice(r1_out);
5869 expected.extend_from_slice(r2_out);
5870 expected.extend_from_slice(r3_out);
5871
5872 assert_eq!(out.as_ref(), expected.as_slice());
5873 }
5874
5875 #[test]
5876 fn pipelined_spillover_without_substitution_stays_zero_copy() {
5877 let config = make_config(vec![make_secret("$KEY", "real-secret", "other.com")]);
5881 let mut handler = SecretsHandler::new(&config, "example.com", true);
5882
5883 let r1 = b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 3\r\n\r\nbod";
5884 let r2 = b"GET /b HTTP/1.1\r\nHost: example.com\r\n\r\n";
5885 let mut chunk = Vec::new();
5886 chunk.extend_from_slice(r1);
5887 chunk.extend_from_slice(r2);
5888
5889 let out = handler.substitute(&chunk).unwrap();
5890 assert!(matches!(out, Cow::Borrowed(_)));
5891 assert_eq!(out.as_ref(), chunk.as_slice());
5892 }
5893
5894 #[test]
5895 fn violation_in_pipelined_next_request_basic_auth_is_detected() {
5896 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5904 let mut handler = SecretsHandler::new(&config, "evil.com", true);
5905
5906 let chunk1 = b"POST /a HTTP/1.1\r\nHost: evil.com\r\nContent-Length: 3\r\n\r\n";
5907 handler.substitute(chunk1).unwrap();
5908
5909 let mut chunk2 = b"foo".to_vec();
5912 chunk2.extend_from_slice(
5913 b"POST /b HTTP/1.1\r\nHost: evil.com\r\nAuthorization: Basic YWRtaW46JEtFWQ==\r\n\r\n",
5914 );
5915 assert_eq!(
5916 handler.substitute(&chunk2).unwrap_err(),
5917 SecretViolationAction::Block
5918 );
5919 }
5920
5921 #[test]
5922 fn pipelined_get_without_content_length_recurses_into_next_request() {
5923 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
5929 let mut handler = SecretsHandler::new(&config, "example.com", true);
5930
5931 let mut chunk = b"GET /a HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
5932 chunk.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
5933
5934 let out = handler.substitute(&chunk).unwrap();
5935
5936 let mut expected = b"GET /a HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
5937 expected.extend_from_slice(
5938 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
5939 );
5940 assert_eq!(out.as_ref(), expected.as_slice());
5941 }
5942
5943 #[test]
5944 fn substitution_resumes_after_chunked_request_body_terminator() {
5945 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
5951 let mut handler = SecretsHandler::new(&config, "example.com", true);
5952
5953 let chunk1 = b"POST /a HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
5955 handler.substitute(chunk1).unwrap();
5956
5957 let mut chunk2 = b"5\r\nhello\r\n0\r\n\r\n".to_vec();
5960 chunk2.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
5961
5962 let out = handler.substitute(&chunk2).unwrap();
5963
5964 let mut expected = b"5\r\nhello\r\n0\r\n\r\n".to_vec();
5965 expected.extend_from_slice(
5966 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
5967 );
5968 assert_eq!(out.as_ref(), expected.as_slice());
5969 }
5970
5971 #[test]
5972 fn exact_host_requires_dns_pin_for_tls_intercepted_secret() {
5973 let ip = Ipv4Addr::new(203, 0, 113, 10);
5974 let shared = SharedState::new(16);
5975 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
5976 let mut handler =
5977 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
5978
5979 let input = b"GET / HTTP/1.1\r\nHost: api.openai.com\r\nAuthorization: Bearer $KEY\r\n\r\n";
5980 assert_eq!(
5981 handler.substitute(input).unwrap_err(),
5982 SecretViolationAction::Block
5983 );
5984
5985 cache_host(&shared, "api.openai.com", ip);
5986 let mut handler =
5987 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
5988 let output = handler.substitute(input).unwrap();
5989
5990 assert!(
5991 String::from_utf8(output.into_owned())
5992 .unwrap()
5993 .contains("real-secret")
5994 );
5995 }
5996
5997 #[test]
5998 fn any_host_bypasses_dns_pin_for_tls_intercepted_secret() {
5999 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
6000 secret.allowed_hosts = vec![HostPattern::Any];
6001 let config = make_config(vec![secret]);
6002 let shared = SharedState::new(16);
6003 let mut handler = SecretsHandler::new_tls_intercepted(
6004 &config,
6005 "unresolved.example",
6006 IpAddr::V4(Ipv4Addr::new(203, 0, 113, 20)),
6007 &shared,
6008 );
6009
6010 let input =
6011 b"GET / HTTP/1.1\r\nHost: unresolved.example\r\nAuthorization: Bearer $KEY\r\n\r\n";
6012 let output = handler.substitute(input).unwrap();
6013
6014 assert!(
6015 String::from_utf8(output.into_owned())
6016 .unwrap()
6017 .contains("real-secret")
6018 );
6019 }
6020
6021 #[test]
6022 fn host_alias_matches_gateway_without_dns_pin() {
6023 let gateway = Ipv4Addr::new(192, 0, 2, 1);
6024 let shared = SharedState::new(16);
6025 shared.set_gateway_ips(Some(gateway), None);
6026
6027 let config = make_config(vec![make_secret("$KEY", "real-secret", crate::HOST_ALIAS)]);
6028 let mut handler = SecretsHandler::new_tls_intercepted(
6029 &config,
6030 crate::HOST_ALIAS,
6031 IpAddr::V4(gateway),
6032 &shared,
6033 );
6034
6035 let input = format!(
6036 "GET / HTTP/1.1\r\nHost: {}\r\nAuthorization: Bearer $KEY\r\n\r\n",
6037 crate::HOST_ALIAS
6038 );
6039 let output = handler.substitute(input.as_bytes()).unwrap();
6040
6041 assert!(
6042 String::from_utf8(output.into_owned())
6043 .unwrap()
6044 .contains("real-secret")
6045 );
6046 }
6047
6048 #[test]
6049 fn tls_ip_authority_compares_addresses_without_allowing_hostname_fronting() {
6050 let shared = SharedState::new(16);
6051 let config = SecretsConfig::default();
6052 for (identity, authority, allowed) in [
6053 ("2001:db8::1", "[2001:db8::1]:443", true),
6054 ("2001:db8::1", "[2001:0db8:0:0:0:0:0:1]:443", true),
6055 ("2001:db8::1", "[2001:db8::2]:443", false),
6056 ("2001:db8::1", "example.com", false),
6057 ("192.0.2.1", "192.0.2.1:443", true),
6058 ("192.0.2.1", "192.0.2.2", false),
6059 ("192.0.2.1", "example.com", false),
6060 ("example.com", "192.0.2.1", false),
6061 ] {
6062 let ip = identity
6063 .parse()
6064 .unwrap_or_else(|_| "192.0.2.1".parse().unwrap());
6065 cache_host(&shared, "example.com", "192.0.2.1".parse().unwrap());
6066 let mut handler = SecretsHandler::new_tls_intercepted(&config, identity, ip, &shared);
6067 let request = format!("GET / HTTP/1.1\r\nHost: {authority}\r\n\r\n");
6068 assert_eq!(
6069 handler.substitute(request.as_bytes()).is_ok(),
6070 allowed,
6071 "identity={identity}, authority={authority}"
6072 );
6073 }
6074 }
6075
6076 #[test]
6077 fn tls_intercepted_http_host_must_match_sni() {
6078 let ip = Ipv4Addr::new(203, 0, 113, 30);
6079 let shared = SharedState::new(16);
6080 cache_host(&shared, "api.openai.com", ip);
6081 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6082 let mut handler =
6083 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6084
6085 let input = b"GET / HTTP/1.1\r\nHost: evil.com\r\nAuthorization: Bearer $KEY\r\n\r\n";
6086 assert_eq!(
6087 handler.substitute(input).unwrap_err(),
6088 SecretViolationAction::Block
6089 );
6090 }
6091
6092 #[test]
6093 fn connect_tls_intercepted_http_host_must_match_sni() {
6094 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6095 let mut handler =
6096 SecretsHandler::new_tls_intercepted_via_connect(&config, "api.openai.com");
6097
6098 let input = b"GET / HTTP/1.1\r\nHost: evil.com\r\nAuthorization: Bearer $KEY\r\n\r\n";
6099 assert_eq!(
6100 handler.substitute(input).unwrap_err(),
6101 SecretViolationAction::Block
6102 );
6103 }
6104
6105 #[test]
6106 fn tls_intercepted_http_host_validation_buffers_split_headers() {
6107 let ip = Ipv4Addr::new(203, 0, 113, 31);
6108 let shared = SharedState::new(16);
6109 cache_host(&shared, "api.openai.com", ip);
6110 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6111 let mut handler =
6112 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6113
6114 let out1 = handler
6115 .substitute(b"GET / HTTP/1.1\r\nHost: evil.com\r\n")
6116 .unwrap();
6117 assert!(out1.is_empty());
6118 assert_eq!(
6119 handler
6120 .substitute(b"Authorization: Bearer $KEY\r\n\r\n")
6121 .unwrap_err(),
6122 SecretViolationAction::Block
6123 );
6124 }
6125
6126 #[test]
6127 fn tls_intercepted_http_host_validation_survives_leading_empty_block() {
6128 let ip = Ipv4Addr::new(203, 0, 113, 32);
6129 let shared = SharedState::new(16);
6130 cache_host(&shared, "api.openai.com", ip);
6131 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6132 let mut handler =
6133 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6134
6135 assert_eq!(
6136 handler.substitute(b"\r\n\r\n").unwrap().as_ref(),
6137 b"\r\n\r\n"
6138 );
6139 assert_eq!(
6140 handler
6141 .substitute(b"GET / HTTP/1.1\r\nHost: evil.com\r\nAuth: $KEY\r\n\r\n")
6142 .unwrap_err(),
6143 SecretViolationAction::Block
6144 );
6145 }
6146
6147 #[test]
6148 fn tls_intercepted_http_host_validation_blocks_leading_empty_request() {
6149 let ip = Ipv4Addr::new(203, 0, 113, 34);
6150 let shared = SharedState::new(16);
6151 cache_host(&shared, "api.openai.com", ip);
6152 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6153 let mut handler =
6154 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6155
6156 let input = b"\r\nGET / HTTP/1.1\r\nHost: evil.com\r\nAuth: $KEY\r\n\r\n";
6157
6158 assert_eq!(
6159 handler.substitute(input).unwrap_err(),
6160 SecretViolationAction::Block
6161 );
6162 }
6163
6164 #[test]
6165 fn tls_intercepted_http_host_validation_buffers_split_leading_empty_request() {
6166 let ip = Ipv4Addr::new(203, 0, 113, 35);
6167 let shared = SharedState::new(16);
6168 cache_host(&shared, "api.openai.com", ip);
6169 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6170 let mut handler =
6171 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6172
6173 let out1 = handler
6174 .substitute(b"\r\nGET / HTTP/1.1\r\nHost: evil.com\r\n")
6175 .unwrap();
6176 assert!(out1.is_empty());
6177 assert_eq!(
6178 handler.substitute(b"Auth: $KEY\r\n\r\n").unwrap_err(),
6179 SecretViolationAction::Block
6180 );
6181 }
6182
6183 #[test]
6184 fn tls_intercepted_http_blocks_malformed_request_line() {
6185 let ip = Ipv4Addr::new(203, 0, 113, 36);
6186 let shared = SharedState::new(16);
6187 cache_host(&shared, "api.openai.com", ip);
6188 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6189 let mut handler =
6190 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6191
6192 let input = b"GET / \r\nHost: api.openai.com\r\nAuth: $KEY\r\n\r\n";
6193
6194 assert_eq!(
6195 handler.substitute(input).unwrap_err(),
6196 SecretViolationAction::Block
6197 );
6198 }
6199
6200 #[test]
6201 fn tls_intercepted_http_absolute_target_must_match_sni() {
6202 let ip = Ipv4Addr::new(203, 0, 113, 37);
6203 let shared = SharedState::new(16);
6204 cache_host(&shared, "api.openai.com", ip);
6205 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6206 let mut handler =
6207 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6208
6209 let input =
6210 b"GET https://evil.com/path HTTP/1.1\r\nHost: api.openai.com\r\nAuth: $KEY\r\n\r\n";
6211
6212 assert_eq!(
6213 handler.substitute(input).unwrap_err(),
6214 SecretViolationAction::Block
6215 );
6216 }
6217
6218 #[test]
6219 fn tls_intercepted_http_absolute_target_allows_matching_sni() {
6220 let ip = Ipv4Addr::new(203, 0, 113, 38);
6221 let shared = SharedState::new(16);
6222 cache_host(&shared, "api.openai.com", ip);
6223 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6224 let mut handler =
6225 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6226
6227 let input = b"GET https://api.openai.com/path HTTP/1.1\r\nHost: api.openai.com\r\nAuth: $KEY\r\n\r\n";
6228 let output = handler.substitute(input).unwrap();
6229
6230 assert!(
6231 String::from_utf8(output.into_owned())
6232 .unwrap()
6233 .contains("real-secret")
6234 );
6235 }
6236
6237 #[test]
6238 fn tls_intercepted_http_duplicate_host_is_blocked() {
6239 let ip = Ipv4Addr::new(203, 0, 113, 39);
6240 let shared = SharedState::new(16);
6241 cache_host(&shared, "api.openai.com", ip);
6242 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6243 let mut handler =
6244 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6245
6246 let input =
6247 b"GET / HTTP/1.1\r\nHost: api.openai.com\r\nHost: api.openai.com\r\nAuth: $KEY\r\n\r\n";
6248
6249 assert_eq!(
6250 handler.substitute(input).unwrap_err(),
6251 SecretViolationAction::Block
6252 );
6253 }
6254
6255 #[test]
6256 fn tls_intercepted_http2_authority_must_match_sni() {
6257 let ip = Ipv4Addr::new(203, 0, 113, 33);
6258 let shared = SharedState::new(16);
6259 cache_host(&shared, "api.openai.com", ip);
6260 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6261 let mut handler =
6262 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6263
6264 let request = h2_request(
6265 &[
6266 (b":method", b"GET"),
6267 (b":scheme", b"https"),
6268 (b":authority", b"evil.com"),
6269 (b":path", b"/"),
6270 (b"authorization", b"Bearer $KEY"),
6271 ],
6272 true,
6273 );
6274
6275 assert_eq!(
6276 handler.substitute(&request).unwrap_err(),
6277 SecretViolationAction::Block
6278 );
6279 }
6280
6281 #[test]
6282 fn connect_tls_intercepted_http2_authority_must_match_sni() {
6283 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6284 let mut handler =
6285 SecretsHandler::new_tls_intercepted_via_connect(&config, "api.openai.com");
6286
6287 let request = h2_request(
6288 &[
6289 (b":method", b"GET"),
6290 (b":scheme", b"https"),
6291 (b":authority", b"evil.com"),
6292 (b":path", b"/"),
6293 (b"authorization", b"Bearer $KEY"),
6294 ],
6295 true,
6296 );
6297
6298 assert_eq!(
6299 handler.substitute(&request).unwrap_err(),
6300 SecretViolationAction::Block
6301 );
6302 }
6303
6304 #[test]
6305 fn http2_trailers_preserve_permitted_placeholders_and_block_other_hosts() {
6306 for (allowed, passthrough) in [(true, false), (false, true), (false, false)] {
6307 let mut secret = make_secret("$KEY", "real-secret", "api.example.com");
6308 if !allowed {
6309 secret.allowed_hosts = vec![HostPattern::Exact("other.example".into())];
6310 }
6311 if passthrough {
6312 secret.passthrough_hosts = vec![HostPattern::Exact("api.example.com".into())];
6313 }
6314 let config = make_config(vec![secret]);
6315 let mut handler = SecretsHandler::new(&config, "api.example.com", true);
6316 let initial = h2_request(
6317 &[
6318 (b":method", b"POST"),
6319 (b":scheme", b"https"),
6320 (b":authority", b"api.example.com"),
6321 (b":path", b"/"),
6322 ],
6323 false,
6324 );
6325 assert!(handler.substitute(&initial).is_ok());
6326 let mut trailers = Vec::new();
6327 append_h2_headers(&mut trailers, 1, &[(b"x-key", b"$KEY")], true);
6328 let result = handler.substitute(&trailers);
6329 if allowed || passthrough {
6330 let mut output = HTTP2_PREFACE.to_vec();
6331 output.extend_from_slice(&result.unwrap());
6332 assert_eq!(
6333 h2_header_value(&decode_first_h2_headers(&output), b"x-key"),
6334 "$KEY"
6335 );
6336 } else {
6337 assert_eq!(result.unwrap_err(), SecretViolationAction::Block);
6338 }
6339 }
6340 }
6341
6342 #[test]
6343 fn tls_intercepted_http2_substitutes_header_secret() {
6344 let ip = Ipv4Addr::new(203, 0, 113, 34);
6345 let shared = SharedState::new(16);
6346 cache_host(&shared, "api.openai.com", ip);
6347 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6348 let mut handler =
6349 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6350
6351 let request = h2_request(
6352 &[
6353 (b":method", b"GET"),
6354 (b":scheme", b"https"),
6355 (b":authority", b"api.openai.com"),
6356 (b":path", b"/"),
6357 (b"authorization", b"Bearer $KEY"),
6358 ],
6359 true,
6360 );
6361
6362 let output = handler.substitute(&request).unwrap().into_owned();
6363 let headers = decode_first_h2_headers(&output);
6364 assert_eq!(
6365 h2_header_value(&headers, b"authorization"),
6366 "Bearer real-secret"
6367 );
6368 }
6369
6370 #[test]
6371 fn tls_intercepted_http2_preface_can_span_tls_reads() {
6372 let ip = Ipv4Addr::new(203, 0, 113, 38);
6373 let shared = SharedState::new(16);
6374 cache_host(&shared, "api.openai.com", ip);
6375 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6376 let mut handler =
6377 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6378
6379 let request = h2_request(
6380 &[
6381 (b":method", b"GET"),
6382 (b":scheme", b"https"),
6383 (b":authority", b"api.openai.com"),
6384 (b":path", b"/"),
6385 (b"authorization", b"Bearer $KEY"),
6386 ],
6387 true,
6388 );
6389
6390 assert_eq!(handler.substitute(&request[..1]).unwrap().as_ref(), b"");
6391
6392 let output = handler.substitute(&request[1..]).unwrap().into_owned();
6393 let headers = decode_first_h2_headers(&output);
6394 assert_eq!(
6395 h2_header_value(&headers, b"authorization"),
6396 "Bearer real-secret"
6397 );
6398 }
6399
6400 #[test]
6401 fn tls_intercepted_http2_substitutes_query_and_basic_auth() {
6402 let ip = Ipv4Addr::new(203, 0, 113, 35);
6403 let shared = SharedState::new(16);
6404 cache_host(&shared, "api.openai.com", ip);
6405 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
6406 secret.substitution = SecretSubstitution {
6407 headers: true,
6408 header_fields: Vec::new(),
6409 query: true,
6410 body: false,
6411 };
6412 let config = make_config(vec![secret]);
6413 let mut handler =
6414 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6415 let auth = format!("Basic {}", BASE64.encode(b"user:$KEY"));
6416
6417 let request = h2_request(
6418 &[
6419 (b":method", b"GET"),
6420 (b":scheme", b"https"),
6421 (b":authority", b"api.openai.com"),
6422 (b":path", b"/v1/chat?token=$KEY"),
6423 (b"authorization", auth.as_bytes()),
6424 ],
6425 true,
6426 );
6427
6428 let output = handler.substitute(&request).unwrap().into_owned();
6429 let headers = decode_first_h2_headers(&output);
6430 assert_eq!(
6431 h2_header_value(&headers, b":path"),
6432 "/v1/chat?token=real-secret"
6433 );
6434 let auth = h2_header_value(&headers, b"authorization");
6435 let decoded = split_auth_scheme(&auth)
6436 .and_then(|(_, encoded)| BASE64.decode(encoded).ok())
6437 .and_then(|bytes| String::from_utf8(bytes).ok())
6438 .unwrap();
6439 assert_eq!(decoded, "user:real-secret");
6440 }
6441
6442 #[test]
6443 fn tls_intercepted_http2_split_header_block_is_validated() {
6444 let ip = Ipv4Addr::new(203, 0, 113, 36);
6445 let shared = SharedState::new(16);
6446 cache_host(&shared, "api.openai.com", ip);
6447 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6448 let mut handler =
6449 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6450
6451 let request = h2_request_with_split_headers(
6452 &[
6453 (b":method", b"GET"),
6454 (b":scheme", b"https"),
6455 (b":authority", b"evil.com"),
6456 (b":path", b"/"),
6457 (b"authorization", b"Bearer $KEY"),
6458 ],
6459 8,
6460 );
6461
6462 assert_eq!(
6463 handler.substitute(&request).unwrap_err(),
6464 SecretViolationAction::Block
6465 );
6466 }
6467
6468 #[test]
6469 fn tls_intercepted_http2_body_placeholder_blocks_until_body_rewrite_exists() {
6470 let ip = Ipv4Addr::new(203, 0, 113, 37);
6471 let shared = SharedState::new(16);
6472 cache_host(&shared, "api.openai.com", ip);
6473 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
6474 secret.substitution.body = true;
6475 let config = make_config(vec![secret]);
6476 let mut handler =
6477 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6478
6479 let request = h2_request_with_data(
6480 &[
6481 (b":method", b"POST"),
6482 (b":scheme", b"https"),
6483 (b":authority", b"api.openai.com"),
6484 (b":path", b"/"),
6485 ],
6486 b"{\"key\":\"$KEY\"}",
6487 );
6488
6489 assert_eq!(
6490 handler.substitute(&request).unwrap_err(),
6491 SecretViolationAction::Block
6492 );
6493 }
6494
6495 #[test]
6496 fn tls_intercepted_http2_body_placeholder_split_across_data_frames_blocks() {
6497 let ip = Ipv4Addr::new(203, 0, 113, 39);
6498 let shared = SharedState::new(16);
6499 cache_host(&shared, "api.openai.com", ip);
6500 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
6501 secret.substitution.body = true;
6502 let config = make_config(vec![secret]);
6503 let mut handler =
6504 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6505
6506 let mut request = HTTP2_PREFACE.to_vec();
6507 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
6508 append_h2_headers(
6509 &mut request,
6510 1,
6511 &[
6512 (b":method", b"POST"),
6513 (b":scheme", b"https"),
6514 (b":authority", b"api.openai.com"),
6515 (b":path", b"/"),
6516 ],
6517 false,
6518 );
6519 append_http2_frame(&mut request, HTTP2_FRAME_DATA, 0, 1, b"$KE").unwrap();
6520 append_http2_frame(
6521 &mut request,
6522 HTTP2_FRAME_DATA,
6523 HTTP2_FLAG_END_STREAM,
6524 1,
6525 b"Y",
6526 )
6527 .unwrap();
6528
6529 assert_eq!(
6530 handler.substitute(&request).unwrap_err(),
6531 SecretViolationAction::Block
6532 );
6533 }
6534
6535 #[test]
6536 fn tls_intercepted_http2_data_tails_are_tracked_per_stream() {
6537 let ip = Ipv4Addr::new(203, 0, 113, 40);
6538 let shared = SharedState::new(16);
6539 cache_host(&shared, "api.openai.com", ip);
6540 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
6541 secret.substitution.body = true;
6542 let config = make_config(vec![secret]);
6543 let mut handler =
6544 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6545
6546 let mut request = HTTP2_PREFACE.to_vec();
6547 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
6548 for stream_id in [1, 3] {
6549 append_h2_headers(
6550 &mut request,
6551 stream_id,
6552 &[
6553 (b":method", b"POST"),
6554 (b":scheme", b"https"),
6555 (b":authority", b"api.openai.com"),
6556 (b":path", b"/"),
6557 ],
6558 false,
6559 );
6560 }
6561 append_http2_frame(&mut request, HTTP2_FRAME_DATA, 0, 1, b"$KE").unwrap();
6562 append_http2_frame(
6563 &mut request,
6564 HTTP2_FRAME_DATA,
6565 HTTP2_FLAG_END_STREAM,
6566 3,
6567 b"Y",
6568 )
6569 .unwrap();
6570
6571 assert!(handler.substitute(&request).is_ok());
6572 }
6573
6574 #[test]
6575 fn tls_intercepted_http2_large_data_frame_without_placeholder_passes() {
6576 let ip = Ipv4Addr::new(203, 0, 113, 41);
6577 let shared = SharedState::new(16);
6578 cache_host(&shared, "api.openai.com", ip);
6579 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
6580 secret.substitution.body = true;
6581 let config = make_config(vec![secret]);
6582 let mut handler =
6583 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6584 let payload = vec![b'a'; 1024 * 1024];
6585
6586 let request = h2_request_with_data(
6587 &[
6588 (b":method", b"POST"),
6589 (b":scheme", b"https"),
6590 (b":authority", b"api.openai.com"),
6591 (b":path", b"/"),
6592 ],
6593 &payload,
6594 );
6595
6596 let output = handler.substitute(&request).unwrap().into_owned();
6597 assert!(output.ends_with(&payload));
6598 }
6599
6600 #[test]
6601 fn tls_intercepted_http2_data_before_headers_is_blocked() {
6602 let ip = Ipv4Addr::new(203, 0, 113, 42);
6603 let shared = SharedState::new(16);
6604 cache_host(&shared, "api.openai.com", ip);
6605 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6606 let mut handler =
6607 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6608
6609 let mut request = HTTP2_PREFACE.to_vec();
6610 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
6611 append_http2_frame(
6612 &mut request,
6613 HTTP2_FRAME_DATA,
6614 HTTP2_FLAG_END_STREAM,
6615 1,
6616 b"body",
6617 )
6618 .unwrap();
6619
6620 assert_eq!(
6621 handler.substitute(&request).unwrap_err(),
6622 SecretViolationAction::Block
6623 );
6624 }
6625
6626 #[test]
6627 fn tls_intercepted_http2_decoded_header_list_size_is_bounded() {
6628 let ip = Ipv4Addr::new(203, 0, 113, 43);
6629 let shared = SharedState::new(16);
6630 cache_host(&shared, "api.openai.com", ip);
6631 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6632 let mut handler =
6633 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6634 let mut encoder = HpackEncoder::with_dynamic_size(4096);
6635
6636 let mut first_block = Vec::new();
6637 for (name, value) in [
6638 (b":method".as_slice(), b"GET".as_slice()),
6639 (b":scheme".as_slice(), b"https".as_slice()),
6640 (b":authority".as_slice(), b"api.openai.com".as_slice()),
6641 (b":path".as_slice(), b"/".as_slice()),
6642 ] {
6643 encoder
6644 .encode(
6645 (name.to_vec(), value.to_vec(), HpackEncoder::NEVER_INDEXED),
6646 &mut first_block,
6647 )
6648 .unwrap();
6649 }
6650 encoder
6651 .encode(
6652 (
6653 b"x-fill".to_vec(),
6654 vec![b'a'; 4000],
6655 HpackEncoder::WITH_INDEXING,
6656 ),
6657 &mut first_block,
6658 )
6659 .unwrap();
6660
6661 let mut second_block = Vec::new();
6662 for (name, value) in [
6663 (b":method".as_slice(), b"GET".as_slice()),
6664 (b":scheme".as_slice(), b"https".as_slice()),
6665 (b":authority".as_slice(), b"api.openai.com".as_slice()),
6666 (b":path".as_slice(), b"/".as_slice()),
6667 ] {
6668 encoder
6669 .encode(
6670 (name.to_vec(), value.to_vec(), HpackEncoder::NEVER_INDEXED),
6671 &mut second_block,
6672 )
6673 .unwrap();
6674 }
6675 for _ in 0..20 {
6676 encoder.encode(62u32, &mut second_block).unwrap();
6677 }
6678
6679 let mut request = HTTP2_PREFACE.to_vec();
6680 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
6681 append_http2_header_frames(&mut request, 1, true, &first_block).unwrap();
6682 append_http2_header_frames(&mut request, 3, true, &second_block).unwrap();
6683
6684 assert_eq!(
6685 handler.substitute(&request).unwrap_err(),
6686 SecretViolationAction::Block
6687 );
6688 }
6689
6690 #[test]
6691 fn tls_intercepted_http2_limits_concurrent_open_streams() {
6692 let ip = Ipv4Addr::new(203, 0, 113, 44);
6693 let shared = SharedState::new(16);
6694 cache_host(&shared, "api.openai.com", ip);
6695 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6696 let mut handler =
6697 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6698
6699 let mut request = HTTP2_PREFACE.to_vec();
6700 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
6701 for i in 0..=MAX_HTTP2_TRACKED_STREAMS {
6702 append_h2_headers(
6703 &mut request,
6704 1 + (i as u32 * 2),
6705 &[
6706 (b":method", b"POST"),
6707 (b":scheme", b"https"),
6708 (b":authority", b"api.openai.com"),
6709 (b":path", b"/"),
6710 ],
6711 false,
6712 );
6713 }
6714
6715 assert_eq!(
6716 handler.substitute(&request).unwrap_err(),
6717 SecretViolationAction::Block
6718 );
6719 }
6720
6721 #[test]
6722 fn tls_intercepted_http2_closed_streams_release_tracking_state() {
6723 let ip = Ipv4Addr::new(203, 0, 113, 45);
6724 let shared = SharedState::new(16);
6725 cache_host(&shared, "api.openai.com", ip);
6726 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6727 let mut handler =
6728 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
6729
6730 let mut request = HTTP2_PREFACE.to_vec();
6731 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
6732 for i in 0..=MAX_HTTP2_TRACKED_STREAMS {
6733 append_h2_headers(
6734 &mut request,
6735 1 + (i as u32 * 2),
6736 &[
6737 (b":method", b"GET"),
6738 (b":scheme", b"https"),
6739 (b":authority", b"api.openai.com"),
6740 (b":path", b"/"),
6741 ],
6742 true,
6743 );
6744 }
6745
6746 assert!(handler.substitute(&request).is_ok());
6747 }
6748
6749 #[test]
6754 fn tls_intercepted_http2_rejects_reused_stream_id_after_headers_end_stream() {
6755 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6756 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
6757 let request = h2_request(
6758 &[
6759 (b":method", b"GET"),
6760 (b":scheme", b"https"),
6761 (b":authority", b"api.openai.com"),
6762 (b":path", b"/first"),
6763 ],
6764 true,
6765 );
6766 handler.substitute(&request).unwrap();
6767
6768 let mut reused = Vec::new();
6769 append_h2_headers(
6770 &mut reused,
6771 1,
6772 &[
6773 (b":method", b"GET"),
6774 (b":scheme", b"https"),
6775 (b":authority", b"api.openai.com"),
6776 (b":path", b"/reused"),
6777 (b"authorization", b"Bearer $KEY"),
6778 ],
6779 true,
6780 );
6781
6782 assert_eq!(
6783 handler.substitute(&reused).unwrap_err(),
6784 SecretViolationAction::Block
6785 );
6786 }
6787
6788 #[test]
6789 fn tls_intercepted_http2_rejects_reused_stream_id_after_data_end_stream() {
6790 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6791 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
6792 let request = h2_request(
6793 &[
6794 (b":method", b"POST"),
6795 (b":scheme", b"https"),
6796 (b":authority", b"api.openai.com"),
6797 (b":path", b"/first"),
6798 ],
6799 false,
6800 );
6801 handler.substitute(&request).unwrap();
6802
6803 let mut completed = Vec::new();
6804 append_http2_frame(
6805 &mut completed,
6806 HTTP2_FRAME_DATA,
6807 HTTP2_FLAG_END_STREAM,
6808 1,
6809 b"",
6810 )
6811 .unwrap();
6812 handler.substitute(&completed).unwrap();
6813
6814 let mut reused = Vec::new();
6815 append_h2_headers(
6816 &mut reused,
6817 1,
6818 &[
6819 (b":method", b"GET"),
6820 (b":scheme", b"https"),
6821 (b":authority", b"api.openai.com"),
6822 (b":path", b"/reused"),
6823 (b"authorization", b"Bearer $KEY"),
6824 ],
6825 true,
6826 );
6827
6828 assert_eq!(
6829 handler.substitute(&reused).unwrap_err(),
6830 SecretViolationAction::Block
6831 );
6832 }
6833
6834 #[test]
6835 fn tls_intercepted_http2_allows_higher_stream_id_after_close() {
6836 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
6837 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
6838 let request = h2_request(
6839 &[
6840 (b":method", b"GET"),
6841 (b":scheme", b"https"),
6842 (b":authority", b"api.openai.com"),
6843 (b":path", b"/first"),
6844 ],
6845 true,
6846 );
6847 handler.substitute(&request).unwrap();
6848
6849 let mut next = Vec::new();
6850 append_h2_headers(
6851 &mut next,
6852 3,
6853 &[
6854 (b":method", b"GET"),
6855 (b":scheme", b"https"),
6856 (b":authority", b"api.openai.com"),
6857 (b":path", b"/next"),
6858 (b"authorization", b"Bearer $KEY"),
6859 ],
6860 true,
6861 );
6862
6863 let output = handler.substitute(&next).unwrap();
6864 let mut framed = HTTP2_PREFACE.to_vec();
6865 framed.extend_from_slice(&output);
6866 let headers = decode_first_h2_headers(&framed);
6867 assert_eq!(
6868 h2_header_value(&headers, b"authorization"),
6869 "Bearer real-secret"
6870 );
6871 }
6872
6873 #[test]
6874 fn chunked_body_internal_terminator_bytes_do_not_end_request() {
6875 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
6876 let mut handler = SecretsHandler::new(&config, "example.com", true);
6877
6878 let chunk1 = b"POST /a HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
6879 handler.substitute(chunk1).unwrap();
6880
6881 let mut chunk2 = b"B\r\nAA\r\n0\r\n\r\nBB\r\n0\r\n\r\n".to_vec();
6882 chunk2.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
6883
6884 let out = handler.substitute(&chunk2).unwrap();
6885
6886 let mut expected = b"B\r\nAA\r\n0\r\n\r\nBB\r\n0\r\n\r\n".to_vec();
6887 expected.extend_from_slice(
6888 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
6889 );
6890 assert_eq!(out.as_ref(), expected.as_slice());
6891 }
6892
6893 #[test]
6894 fn split_chunked_terminator_resumes_next_request() {
6895 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
6896 let mut handler = SecretsHandler::new(&config, "example.com", true);
6897
6898 let chunk1 = b"POST /a HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
6899 handler.substitute(chunk1).unwrap();
6900
6901 let chunk2 = b"5\r\nhello\r\n0\r";
6902 let out2 = handler.substitute(chunk2).unwrap();
6903 assert_eq!(out2.as_ref(), chunk2.as_slice());
6904
6905 let mut chunk3 = b"\n\r\n".to_vec();
6906 chunk3.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
6907
6908 let out3 = handler.substitute(&chunk3).unwrap();
6909
6910 let mut expected = b"\n\r\n".to_vec();
6911 expected.extend_from_slice(
6912 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
6913 );
6914 assert_eq!(out3.as_ref(), expected.as_slice());
6915 }
6916
6917 fn append_h2_raw_block(out: &mut Vec<u8>, stream_id: u32, block: &[u8], split_at: usize) {
6920 let end = HTTP2_FLAG_END_HEADERS | HTTP2_FLAG_END_STREAM;
6921 if split_at == 0 || split_at >= block.len() {
6922 append_http2_frame(out, HTTP2_FRAME_HEADERS, end, stream_id, block).unwrap();
6923 return;
6924 }
6925 let (head, tail) = block.split_at(split_at);
6926 append_http2_frame(
6927 out,
6928 HTTP2_FRAME_HEADERS,
6929 HTTP2_FLAG_END_STREAM,
6930 stream_id,
6931 head,
6932 )
6933 .unwrap();
6934 append_http2_frame(out, HTTP2_FRAME_CONTINUATION, end, stream_id, tail).unwrap();
6935 }
6936
6937 #[test]
6938 fn http2_malformed_hpack_block_is_blocked() {
6939 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
6942 for block in PANICKING_BLOCKS {
6943 for split_at in 0..block.len() {
6944 let mut request = HTTP2_PREFACE.to_vec();
6945 append_h2_raw_block(&mut request, 1, block, split_at);
6946 let handlers = [
6947 SecretsHandler::new(&config, "a.example", false),
6948 SecretsHandler::new(&config, "a.example", true),
6949 plain_http_policy_handler(&make_config(Vec::new())),
6952 ];
6953 for mut handler in handlers {
6954 assert_eq!(
6955 handler.substitute(&request).unwrap_err(),
6956 SecretViolationAction::Block,
6957 "{block:02x?} split at {split_at}"
6958 );
6959 }
6960 }
6961 }
6962 }
6963
6964 #[test]
6965 fn http2_random_header_blocks_never_panic() {
6966 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
6967 let request_prefix = encode_h2_header_block(&[
6968 (b":method", b"GET"),
6969 (b":scheme", b"https"),
6970 (b":authority", b"a.example"),
6971 (b":path", b"/"),
6972 ]);
6973 let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
6974 let mut processed = [0usize; 3];
6975 for _ in 0..500 {
6976 let mut request = HTTP2_PREFACE.to_vec();
6981 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
6982 for stream_id in [1, 3, 5].into_iter().take(1 + rng.below(3)) {
6983 let mut block = Vec::new();
6984 if rng.below(2) == 0 {
6985 block.extend_from_slice(&request_prefix);
6986 }
6987 block.extend(random_block(&mut rng));
6988 let split_at = rng.below(block.len() + 1);
6989 append_h2_raw_block(&mut request, stream_id, &block, split_at);
6990 }
6991 let handlers = [
6992 SecretsHandler::new(&config, "a.example", false),
6993 SecretsHandler::new(&config, "a.example", true),
6994 plain_http_policy_handler(&make_config(Vec::new())),
6995 ];
6996 for (index, mut handler) in handlers.into_iter().enumerate() {
6997 if handler.substitute(&request).is_ok() {
6998 processed[index] += 1;
6999 }
7000 }
7001 }
7002 for (index, count) in processed.iter().enumerate() {
7006 assert!(
7007 *count > 0,
7008 "handler {index} never processed a random request successfully"
7009 );
7010 }
7011 }
7012
7013 #[test]
7014 fn http2_dynamic_table_reference_across_streams_is_decoded() {
7015 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
7021 let fields: [(&[u8], &[u8]); 4] = [
7022 (b":method", b"POST"),
7023 (b":scheme", b"https"),
7024 (b":authority", b"a.example"),
7025 (b":path", b"/"),
7026 ];
7027 let mut request_block = encode_h2_header_block(&fields);
7028 request_block.extend_from_slice(&[0x40, 0x03, b'x', b'-', b'a', 0x01, b'1']);
7029 let mut reference_block = encode_h2_header_block(&fields);
7030 reference_block.push(0xbe);
7031
7032 let mut handler = SecretsHandler::new(&config, "a.example", false);
7033 let mut request = HTTP2_PREFACE.to_vec();
7034 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
7035 append_http2_header_frames(&mut request, 1, true, &request_block).unwrap();
7036 handler.substitute(&request).unwrap();
7037
7038 let mut next = Vec::new();
7039 append_http2_header_frames(&mut next, 3, true, &reference_block).unwrap();
7040 let mut output = HTTP2_PREFACE.to_vec();
7041 output.extend_from_slice(&handler.substitute(&next).unwrap());
7042 assert_eq!(
7043 h2_header_value(&decode_first_h2_headers(&output), b"x-a"),
7044 "1"
7045 );
7046 }
7047
7048 #[test]
7049 fn tls_intercepted_http2_output_is_unchanged_for_indexed_guest_blocks() {
7050 let ip = Ipv4Addr::new(203, 0, 113, 45);
7051 let shared = SharedState::new(16);
7052 cache_host(&shared, "api.openai.com", ip);
7053 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
7054 let mut handler =
7055 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
7056
7057 let flags = HpackEncoder::WITH_INDEXING
7060 | HpackEncoder::HUFFMAN_NAME
7061 | HpackEncoder::HUFFMAN_VALUE
7062 | HpackEncoder::BEST_FORMAT;
7063 let fields: [(&[u8], &[u8]); 5] = [
7064 (b":method", b"GET"),
7065 (b":scheme", b"https"),
7066 (b":authority", b"api.openai.com"),
7067 (b":path", b"/"),
7068 (b"authorization", b"Bearer $KEY"),
7069 ];
7070 let mut encoder = HpackEncoder::with_dynamic_size(4096);
7071 let mut request = HTTP2_PREFACE.to_vec();
7072 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
7073 for stream_id in [1, 3] {
7074 let mut block = Vec::new();
7075 for (name, value) in fields {
7076 encoder
7077 .encode((name.to_vec(), value.to_vec(), flags), &mut block)
7078 .unwrap();
7079 }
7080 if stream_id == 3 {
7081 assert!(block.iter().all(|octet| octet & 0x80 != 0), "{block:02x?}");
7082 }
7083 append_http2_header_frames(&mut request, stream_id, true, &block).unwrap();
7084 }
7085
7086 let output = handler.substitute(&request).unwrap().into_owned();
7087
7088 let mut substituted = fields;
7091 substituted[4].1 = b"Bearer real-secret";
7092 let mut expected = HTTP2_PREFACE.to_vec();
7093 append_http2_frame(&mut expected, 0x4, 0, 0, &[]).unwrap();
7094 append_h2_headers(&mut expected, 1, &substituted, true);
7095 append_h2_headers(&mut expected, 3, &substituted, true);
7096 assert_eq!(output, expected);
7097 }
7098
7099 #[test]
7100 fn http2_size_update_only_trailer_block_is_accepted_and_applied() {
7101 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
7106 let fields: [(&[u8], &[u8]); 4] = [
7107 (b":method", b"POST"),
7108 (b":scheme", b"https"),
7109 (b":authority", b"a.example"),
7110 (b":path", b"/"),
7111 ];
7112 let mut request_block = encode_h2_header_block(&fields);
7113 request_block.extend_from_slice(&[0x40, 0x03, b'x', b'-', b'a', 0x01, b'1']);
7114 let mut next_block = encode_h2_header_block(&fields);
7115 next_block.push(0xbe);
7116
7117 for (trailer, evicted) in [
7118 (&[][..], false),
7119 (&[0x20][..], true),
7120 (&[0x20, 0x3f, 0xe1, 0x1f][..], true),
7121 ] {
7122 let mut handler = SecretsHandler::new(&config, "a.example", false);
7123 let mut request = HTTP2_PREFACE.to_vec();
7124 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
7125 append_http2_header_frames(&mut request, 1, false, &request_block).unwrap();
7126 append_http2_header_frames(&mut request, 1, true, trailer).unwrap();
7127 let output = handler.substitute(&request).unwrap().into_owned();
7128 let mut empty_trailer = Vec::new();
7130 append_http2_header_frames(&mut empty_trailer, 1, true, &[]).unwrap();
7131 assert!(output.ends_with(&empty_trailer), "{trailer:02x?}");
7132
7133 let mut next = Vec::new();
7134 append_http2_header_frames(&mut next, 3, true, &next_block).unwrap();
7135 let result = handler.substitute(&next);
7136 if evicted {
7137 assert_eq!(
7138 result.unwrap_err(),
7139 SecretViolationAction::Block,
7140 "{trailer:02x?}"
7141 );
7142 } else {
7143 let mut output = HTTP2_PREFACE.to_vec();
7144 output.extend_from_slice(&result.unwrap());
7145 assert_eq!(
7146 h2_header_value(&decode_first_h2_headers(&output), b"x-a"),
7147 "1"
7148 );
7149 }
7150 }
7151 }
7152
7153 #[test]
7154 fn http2_header_block_limit_is_64_kib_across_continuation_frames() {
7155 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
7160 for extra in [0, 1] {
7161 let mut request = h2_request(
7162 &[
7163 (b":method", b"POST"),
7164 (b":scheme", b"https"),
7165 (b":authority", b"a.example"),
7166 (b":path", b"/"),
7167 ],
7168 false,
7169 );
7170 let value = vec![b'v'; 64 * 1024 - 12 + extra];
7171 let block = encode_h2_header_block(&[(b"x-fill", &value)]);
7172 assert_eq!(block.len(), MAX_HTTP2_HEADER_BLOCK_BYTES + extra);
7173 append_http2_header_frames(&mut request, 1, true, &block).unwrap();
7174
7175 let mut handler = SecretsHandler::new(&config, "a.example", false);
7176 let result = handler.substitute(&request);
7177 if extra == 0 {
7178 assert_eq!(result.unwrap().into_owned(), request);
7180 } else {
7181 assert_eq!(result.unwrap_err(), SecretViolationAction::Block);
7182 }
7183 }
7184 }
7185
7186 fn h2_request_block_with_huffman_value(value_len: usize) -> Vec<u8> {
7193 let mut block = encode_h2_header_block(&[
7194 (b":method", b"GET"),
7195 (b":scheme", b"https"),
7196 (b":authority", b"a.example"),
7197 (b":path", b"/"),
7198 ]);
7199 let mut encoder = HpackEncoder::with_dynamic_size(4096);
7200 encoder
7201 .encode(
7202 (
7203 b"a".to_vec(),
7204 vec![b'a'; value_len],
7205 HpackEncoder::NEVER_INDEXED | HpackEncoder::HUFFMAN_VALUE,
7206 ),
7207 &mut block,
7208 )
7209 .unwrap();
7210 block
7211 }
7212
7213 fn h2_size_update_storm_block(unit: &[u8]) -> Vec<u8> {
7215 let mut block = Vec::new();
7216 while block.len() + unit.len() <= MAX_HTTP2_HEADER_BLOCK_BYTES {
7217 block.extend_from_slice(unit);
7218 }
7219 block
7220 }
7221
7222 #[test]
7223 fn http2_header_field_count_boundary_is_1024() {
7224 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
7227 for (extra, accepted) in [(1020usize, true), (1021, false)] {
7228 let mut fields: Vec<(&[u8], &[u8])> = vec![
7229 (b":method", b"GET"),
7230 (b":scheme", b"https"),
7231 (b":authority", b"a.example"),
7232 (b":path", b"/"),
7233 ];
7234 for _ in 0..extra {
7235 fields.push((b"x-a", b"1"));
7236 }
7237 let request = h2_request(&fields, true);
7238 let mut handler = SecretsHandler::new(&config, "a.example", false);
7239 let result = handler.substitute(&request);
7240 if accepted {
7241 assert!(result.is_ok(), "1,024 fields must be accepted");
7242 } else {
7243 assert_eq!(result.unwrap_err(), SecretViolationAction::Block);
7244 }
7245 }
7246 }
7247
7248 #[test]
7249 fn http2_decoded_size_boundary_is_exact_for_huffman_values() {
7250 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
7255 for (value_len, accepted) in [(65_468usize, true), (65_469, false)] {
7256 let block = h2_request_block_with_huffman_value(value_len);
7257 assert!(block.len() <= MAX_HTTP2_HEADER_BLOCK_BYTES);
7258 let mut request = HTTP2_PREFACE.to_vec();
7259 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
7260 append_http2_header_frames(&mut request, 1, true, &block).unwrap();
7261 let mut handler = SecretsHandler::new(&config, "a.example", false);
7262 let result = handler.substitute(&request);
7263 if accepted {
7264 assert!(result.is_ok(), "decoded size at the limit must be accepted");
7265 } else {
7266 assert_eq!(result.unwrap_err(), SecretViolationAction::Block);
7267 }
7268 }
7269 }
7270
7271 #[test]
7272 fn http2_block_under_the_cap_can_expand_past_the_decoded_limit() {
7273 let block = h2_request_block_with_huffman_value(100_000);
7277 assert!(block.len() <= MAX_HTTP2_HEADER_BLOCK_BYTES);
7278 let mut request = HTTP2_PREFACE.to_vec();
7279 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
7280 append_http2_header_frames(&mut request, 1, true, &block).unwrap();
7281 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
7282 let mut handler = SecretsHandler::new(&config, "a.example", false);
7283 assert_eq!(
7284 handler.substitute(&request).unwrap_err(),
7285 SecretViolationAction::Block
7286 );
7287 }
7288
7289 #[test]
7290 fn http2_size_update_storm_is_bounded_and_terminates() {
7291 let config = make_config(vec![make_secret("$KEY", "real-secret", "a.example")]);
7299 let request_block = encode_h2_header_block(&[
7300 (b":method", b"POST"),
7301 (b":scheme", b"https"),
7302 (b":authority", b"a.example"),
7303 (b":path", b"/"),
7304 ]);
7305
7306 for unit in [&[0x20u8][..], &[0x20, 0x3f, 0xe1, 0x1f][..]] {
7308 let trailer = h2_size_update_storm_block(unit);
7309 let mut handler = SecretsHandler::new(&config, "a.example", false);
7310 let mut request = HTTP2_PREFACE.to_vec();
7311 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
7312 append_http2_header_frames(&mut request, 1, false, &request_block).unwrap();
7313 append_http2_header_frames(&mut request, 1, true, &trailer).unwrap();
7314 let output = handler.substitute(&request).unwrap();
7315 let mut empty_trailer = Vec::new();
7318 append_http2_header_frames(&mut empty_trailer, 1, true, &[]).unwrap();
7319 assert!(output.ends_with(&empty_trailer), "{unit:02x?}");
7320 }
7321
7322 let mut interspersed = request_block;
7327 let mut fields = 4usize;
7328 while interspersed.len() < MAX_HTTP2_HEADER_BLOCK_BYTES {
7329 if fields < MAX_HTTP2_HEADER_FIELDS && interspersed.len().is_multiple_of(64) {
7330 interspersed.push(0xa0);
7332 fields += 1;
7333 } else {
7334 interspersed.push(0x20);
7335 }
7336 }
7337 let mut handler = SecretsHandler::new(&config, "a.example", false);
7338 let mut request = HTTP2_PREFACE.to_vec();
7339 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
7340 append_http2_header_frames(&mut request, 1, true, &interspersed).unwrap();
7341 assert!(handler.substitute(&request).is_ok());
7342 }
7343}