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