1use std::borrow::Cow;
7use std::collections::{HashMap, HashSet};
8use std::net::IpAddr;
9
10use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
11use httlib_hpack::{Decoder as HpackDecoder, Encoder as HpackEncoder};
12use percent_encoding::percent_decode;
13
14use super::config::{
15 HostPattern, MAX_SECRET_PLACEHOLDER_BYTES, SecretEntry, SecretsConfig, ViolationAction,
16};
17use crate::shared::SharedState;
18
19const MAX_HTTP_HEADER_BYTES: usize = 64 * 1024;
25
26const MAX_HTTP_BODY_BUFFER_BYTES: usize = 16 * 1024 * 1024;
28
29const HTTP2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
31
32const MAX_HTTP2_FRAME_PAYLOAD_BYTES: usize = 0x00ff_ffff;
36
37const MAX_HTTP2_HEADER_BLOCK_BYTES: usize = 64 * 1024;
39
40const MAX_HTTP2_DECODED_HEADER_BYTES: usize = 64 * 1024;
42
43const MAX_HTTP2_HEADER_FIELDS: usize = 1024;
45
46const MAX_HTTP2_TRACKED_STREAMS: usize = 1024;
48
49const HTTP2_OUTBOUND_FRAME_PAYLOAD_BYTES: usize = 16 * 1024;
52
53const HTTP2_FRAME_DATA: u8 = 0x0;
54const HTTP2_FRAME_HEADERS: u8 = 0x1;
55const HTTP2_FRAME_PUSH_PROMISE: u8 = 0x5;
56const HTTP2_FRAME_CONTINUATION: u8 = 0x9;
57
58const HTTP2_FLAG_END_STREAM: u8 = 0x1;
59const HTTP2_FLAG_END_HEADERS: u8 = 0x4;
60const HTTP2_FLAG_PADDED: u8 = 0x8;
61const HTTP2_FLAG_PRIORITY: u8 = 0x20;
62
63pub struct SecretsHandler {
72 eligible_for_substitution: Vec<EligibleSecret>,
74 ineligible_for_substitution: Vec<IneligibleSecret>,
76 tls_intercepted: bool,
78 max_detection_window_len: usize,
81 max_body_placeholder_len: usize,
84 placeholder_limit_exceeded: bool,
86 prev_tail: Vec<u8>,
90 http_state: HttpState,
94 http_sni: Option<String>,
96 http_pending: Vec<u8>,
99 http2_state: Option<Http2State>,
101}
102
103#[derive(Debug, Clone)]
105enum HttpState {
106 AwaitingHeaders,
108 InBody { remaining: usize },
111 InChunkedBody { state: ChunkedBodyState },
113 InChunkedRewriteBody { state: ChunkedRewriteState },
116 BufferingBody { remaining: usize },
119}
120
121#[derive(Debug, Clone, Default)]
123struct ChunkedBodyState {
124 phase: ChunkedPhase,
125 line: Vec<u8>,
126 decoded_tail: Vec<u8>,
127}
128
129#[derive(Debug, Clone, Default)]
131struct ChunkedRewriteState {
132 parser: ChunkedBodyState,
133 substitution_tail: Vec<u8>,
134}
135
136struct Http2State {
138 preface_seen: bool,
139 buffer: Vec<u8>,
140 header_block: Option<Http2HeaderBlock>,
141 open_request_streams: HashSet<u32>,
142 data_tails: HashMap<u32, Vec<u8>>,
143 decoder: HpackDecoder<'static>,
144 encoder: HpackEncoder<'static>,
145}
146
147struct Http2HeaderBlock {
149 stream_id: u32,
150 end_stream: bool,
151 block: Vec<u8>,
152}
153
154struct Http2Frame<'a> {
156 kind: u8,
157 flags: u8,
158 stream_id: u32,
159 payload: &'a [u8],
160 raw: &'a [u8],
161}
162
163type Http2Headers = Vec<(Vec<u8>, Vec<u8>)>;
164
165#[derive(Debug, Clone, Default)]
167enum ChunkedPhase {
168 #[default]
170 SizeLine,
171 Data { remaining: usize },
173 DataCrlf { seen_cr: bool },
175 TrailerLine,
177}
178
179struct SecretHostIdentity<'a> {
181 guest_ip: IpAddr,
182 shared: &'a SharedState,
183}
184
185struct HttpRequestMetadata {
187 host_headers: Vec<String>,
188}
189
190struct RequestFraming {
192 state: HttpState,
193 body_in_request: usize,
194 body_substitution_allowed: bool,
195}
196
197struct ChunkedRewriteResult {
199 output: Vec<u8>,
200 body_end: Option<usize>,
201}
202
203enum ChunkedBodyEvent<'a> {
205 Payload(&'a [u8]),
206 ZeroChunk,
207 TrailerLine(&'a [u8]),
208}
209
210struct EligibleSecret {
212 placeholder: String,
213 value: String,
214 inject_headers: bool,
215 inject_basic_auth: bool,
216 inject_query_params: bool,
217 inject_body: bool,
218 require_tls_identity: bool,
219}
220
221struct IneligibleSecret {
223 placeholder: String,
224 action: BlockingAction,
225}
226
227#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
229enum BlockingAction {
230 Block,
231 #[default]
232 BlockAndLog,
233 BlockAndTerminate,
234}
235
236impl EligibleSecret {
241 fn wants_header_injection(&self) -> bool {
244 self.inject_headers || self.inject_basic_auth || self.inject_query_params
245 }
246
247 fn may_substitute_in_headers(&self, headers: &[u8]) -> bool {
250 if !self.wants_header_injection() {
251 return false;
252 }
253
254 let needle = self.placeholder.as_bytes();
255 if (self.inject_headers || self.inject_query_params) && contains_bytes(headers, needle) {
256 return true;
257 }
258
259 if self.inject_basic_auth {
261 return basic_auth_decoded_contains(
262 String::from_utf8_lossy(headers).as_ref(),
263 &self.placeholder,
264 );
265 }
266
267 false
268 }
269
270 fn substitute_in_headers(&self, headers: &str) -> String {
273 let mut result = String::with_capacity(headers.len());
274 for (i, line) in headers.split("\r\n").enumerate() {
275 if i > 0 {
276 result.push_str("\r\n");
277 }
278 match self.substitute_in_header_line(line, i == 0) {
279 Some(s) => result.push_str(&s),
280 None => result.push_str(line),
281 }
282 }
283 result
284 }
285
286 fn substitute_in_header_line(&self, line: &str, is_request_line: bool) -> Option<String> {
290 if is_request_line {
291 return self
292 .inject_query_params
293 .then(|| substitute_query_in_request_line(line, &self.placeholder, &self.value))
294 .flatten();
295 }
296
297 if self.inject_basic_auth
298 && is_authorization_header(line)
299 && let Some(replaced) = self.substitute_basic_auth_header(line)
300 {
301 return Some(replaced);
302 }
303 if self.inject_headers {
304 return Some(line.replace(&self.placeholder, &self.value));
305 }
306 None
307 }
308
309 fn substitute_basic_auth_header(&self, line: &str) -> Option<String> {
315 let decoded = decode_basic_credentials(line)?;
316 if !decoded.contains(&self.placeholder) {
317 return None;
318 }
319 let (name, _) = line.split_once(':')?;
320 let replaced = decoded.replace(&self.placeholder, &self.value);
321 Some(format!(
322 "{name}: Basic {}",
323 BASE64.encode(replaced.as_bytes())
324 ))
325 }
326}
327
328impl BlockingAction {
329 fn from_violation_action(action: &ViolationAction) -> Option<Self> {
330 match action {
331 ViolationAction::Block => Some(Self::Block),
332 ViolationAction::BlockAndLog => Some(Self::BlockAndLog),
333 ViolationAction::BlockAndTerminate => Some(Self::BlockAndTerminate),
334 ViolationAction::Passthrough(_) => None,
335 }
336 }
337
338 fn into_violation_action(self) -> ViolationAction {
339 match self {
340 Self::Block => ViolationAction::Block,
341 Self::BlockAndLog => ViolationAction::BlockAndLog,
342 Self::BlockAndTerminate => ViolationAction::BlockAndTerminate,
343 }
344 }
345}
346
347impl Default for Http2State {
348 fn default() -> Self {
349 Self {
350 preface_seen: false,
351 buffer: Vec::new(),
352 header_block: None,
353 open_request_streams: HashSet::new(),
354 data_tails: HashMap::new(),
355 decoder: HpackDecoder::with_dynamic_size(4096),
356 encoder: HpackEncoder::with_dynamic_size(4096),
357 }
358 }
359}
360
361impl SecretsHandler {
362 pub fn new(config: &SecretsConfig, sni: &str, tls_intercepted: bool) -> Self {
369 Self::new_inner(config, sni, tls_intercepted, None)
370 }
371
372 pub fn new_tls_intercepted(
377 config: &SecretsConfig,
378 sni: &str,
379 guest_ip: IpAddr,
380 shared: &SharedState,
381 ) -> Self {
382 Self::new_inner(
383 config,
384 sni,
385 true,
386 Some(SecretHostIdentity { guest_ip, shared }),
387 )
388 }
389
390 fn new_inner(
391 config: &SecretsConfig,
392 sni: &str,
393 tls_intercepted: bool,
394 identity: Option<SecretHostIdentity<'_>>,
395 ) -> Self {
396 let enforce_http_authority = identity.is_some();
397 let mut eligible_for_substitution = Vec::new();
398 let mut ineligible_for_substitution = Vec::new();
399 let mut max_detection_window_len = 0;
400 let mut max_body_placeholder_len = 0;
401 let mut placeholder_limit_exceeded = false;
402
403 for secret in &config.secrets {
404 if secret.placeholder.len() > MAX_SECRET_PLACEHOLDER_BYTES {
405 placeholder_limit_exceeded = true;
406 }
407 max_detection_window_len = max_detection_window_len.max(max_placeholder_detection_len(
408 secret.placeholder.len().min(MAX_SECRET_PLACEHOLDER_BYTES),
409 ));
410
411 let host_allowed = secret_host_allowed(secret, sni, identity.as_ref());
412
413 if host_allowed {
416 if secret.injection.body {
417 max_body_placeholder_len = max_body_placeholder_len
418 .max(secret.placeholder.len().min(MAX_SECRET_PLACEHOLDER_BYTES));
419 }
420 eligible_for_substitution.push(EligibleSecret {
421 placeholder: secret.placeholder.clone(),
422 value: secret.value.clone(),
423 inject_headers: secret.injection.headers,
424 inject_basic_auth: secret.injection.basic_auth,
425 inject_query_params: secret.injection.query_params,
426 inject_body: secret.injection.body,
427 require_tls_identity: secret.require_tls_identity,
428 });
429
430 continue;
431 }
432
433 let action = effective_violation_action(secret, config, sni, identity.as_ref());
434
435 if let ViolationAction::Passthrough(hosts) = action
437 && hosts
438 .iter()
439 .any(|p| host_pattern_allowed(p, sni, identity.as_ref()))
440 {
441 continue;
442 }
443
444 ineligible_for_substitution.push(IneligibleSecret {
446 placeholder: secret.placeholder.clone(),
447 action: BlockingAction::from_violation_action(action).unwrap_or_default(),
448 });
449 }
450
451 Self {
452 eligible_for_substitution,
453 ineligible_for_substitution,
454 tls_intercepted,
455 max_detection_window_len,
456 max_body_placeholder_len,
457 placeholder_limit_exceeded,
458 prev_tail: Vec::new(),
459 http_state: HttpState::AwaitingHeaders,
460 http_sni: enforce_http_authority.then(|| sni.to_string()),
461 http_pending: Vec::new(),
462 http2_state: None,
463 }
464 }
465
466 pub fn substitute<'a>(&mut self, data: &'a [u8]) -> Result<Cow<'a, [u8]>, ViolationAction> {
477 if self.placeholder_limit_exceeded {
478 tracing::error!(
479 "secret configuration rejected: placeholder exceeds {} bytes",
480 MAX_SECRET_PLACEHOLDER_BYTES
481 );
482 return Err(ViolationAction::Block);
483 }
484
485 if self.http2_state.is_some() {
486 return self.substitute_http2(data);
487 }
488
489 if self.http_pending.is_empty() {
490 if has_complete_http2_preface(data) {
491 self.http2_state = Some(Http2State::default());
492 return self.substitute_http2(data);
493 }
494 if is_http2_preface_prefix(data) {
495 self.http_pending.extend_from_slice(data);
496 return Ok(Cow::Owned(Vec::new()));
497 }
498 } else {
499 let mut pending_prefix = Vec::with_capacity(self.http_pending.len() + data.len());
500 pending_prefix.extend_from_slice(&self.http_pending);
501 pending_prefix.extend_from_slice(data);
502 if has_complete_http2_preface(&pending_prefix) {
503 self.http_pending.clear();
504 self.http2_state = Some(Http2State::default());
505 return self.substitute_http2(&pending_prefix);
506 }
507 if is_http2_preface_prefix(&pending_prefix) {
508 self.http_pending = pending_prefix;
509 return Ok(Cow::Owned(Vec::new()));
510 }
511 }
512
513 match std::mem::replace(&mut self.http_state, HttpState::AwaitingHeaders) {
514 HttpState::BufferingBody { remaining } => {
515 return self.substitute_buffered_body(data, remaining);
516 }
517 HttpState::InBody { remaining } => {
518 return self.substitute_body_chunk(data, remaining);
519 }
520 HttpState::InChunkedBody { state } => {
521 return self.substitute_chunked_body_chunk(data, state);
522 }
523 HttpState::InChunkedRewriteBody { state } => {
524 return self.substitute_chunked_rewrite_body_chunk(data, state);
525 }
526 HttpState::AwaitingHeaders => {}
527 }
528
529 if !self.http_pending.is_empty() {
530 self.http_pending.extend_from_slice(data);
531 if self.http_pending.len() > MAX_HTTP_HEADER_BYTES {
532 return Err(ViolationAction::Block);
533 }
534 if find_header_boundary(&self.http_pending).is_none() {
535 if first_line_is_not_http_request(&self.http_pending)
536 || !looks_like_http_request_prefix(&self.http_pending)
537 {
538 let pending = std::mem::take(&mut self.http_pending);
539 let output = self.substitute_ready(&pending)?.into_owned();
540 return Ok(Cow::Owned(output));
541 }
542 return Ok(Cow::Owned(Vec::new()));
543 }
544
545 let pending = std::mem::take(&mut self.http_pending);
546 let output = self.substitute_ready(&pending)?.into_owned();
547 return Ok(Cow::Owned(output));
548 }
549
550 if find_header_boundary(data).is_none()
551 && looks_like_http_request_prefix(data)
552 && !first_line_is_not_http_request(data)
553 {
554 if data.len() > MAX_HTTP_HEADER_BYTES {
555 return Err(ViolationAction::Block);
556 }
557 self.http_pending.extend_from_slice(data);
558 return Ok(Cow::Owned(Vec::new()));
559 }
560
561 self.substitute_ready(data)
562 }
563
564 fn substitute_http2<'a>(&mut self, data: &[u8]) -> Result<Cow<'a, [u8]>, ViolationAction> {
565 let mut state = self.http2_state.take().unwrap_or_default();
566 let output = state.process(self, data)?;
567 self.http2_state = Some(state);
568 Ok(Cow::Owned(output))
569 }
570
571 fn substitute_ready<'a>(&mut self, data: &'a [u8]) -> Result<Cow<'a, [u8]>, ViolationAction> {
572 let boundary = find_header_boundary(data);
575 let (header_bytes, after_headers) = match boundary {
576 Some(pos) => (&data[..pos], &data[pos..]),
577 None => (data, &[] as &[u8]),
578 };
579
580 let mut body_substitution_allowed = false;
586 let (body_bytes, spillover) = if boundary.is_some() {
587 let header_text = String::from_utf8_lossy(header_bytes);
588 if let Some(sni) = self.http_sni.as_deref()
589 && let Some(metadata) = parse_http_request_metadata(header_bytes)?
590 && !metadata
591 .host_headers
592 .iter()
593 .all(|host| authority_matches_sni(host, sni))
594 {
595 return Err(ViolationAction::Block);
596 }
597
598 if is_transfer_chunked(header_text.as_ref()) {
599 return self.substitute_chunked_ready(
600 data,
601 header_bytes,
602 after_headers,
603 header_text.as_ref(),
604 );
605 }
606
607 let framing = next_state_after_headers(header_text.as_ref(), after_headers)?;
608 if self.needs_body_injection()
609 && framing.body_substitution_allowed
610 && content_length_exceeds_buffer_limit(header_text.as_ref())?
611 {
612 return Err(ViolationAction::Block);
613 }
614 if self.needs_body_injection()
615 && framing.body_substitution_allowed
616 && let HttpState::InBody { remaining } = &framing.state
617 {
618 self.http_pending.extend_from_slice(data);
619 self.http_state = HttpState::BufferingBody {
620 remaining: *remaining,
621 };
622 return Ok(Cow::Owned(Vec::new()));
623 }
624
625 body_substitution_allowed = framing.body_substitution_allowed;
626 self.http_state = framing.state;
627 after_headers.split_at(framing.body_in_request)
628 } else {
629 (after_headers, &[] as &[u8])
630 };
631
632 let this_request = &data[..header_bytes.len() + body_bytes.len()];
634
635 if let Some(action) = self
637 .detect_blocking_action(this_request, String::from_utf8_lossy(header_bytes).as_ref())
638 {
639 match action {
640 BlockingAction::Block => return Err(action.into_violation_action()),
641 BlockingAction::BlockAndLog => {
642 tracing::warn!("secret violation: placeholder detected for disallowed host");
643 return Err(action.into_violation_action());
644 }
645 BlockingAction::BlockAndTerminate => {
646 tracing::error!(
647 "secret violation: placeholder detected for disallowed host - terminating"
648 );
649 return Err(action.into_violation_action());
650 }
651 }
652 }
653 self.update_tail(this_request);
654
655 if self.eligible_for_substitution.is_empty() {
656 return self.append_pipelined_spillover(data, this_request, spillover);
659 }
660
661 let mut header_str = None;
663 let mut body = None;
664
665 for secret in &self.eligible_for_substitution {
666 if secret.require_tls_identity && !self.tls_intercepted {
668 continue;
669 }
670
671 if secret.may_substitute_in_headers(header_bytes) {
673 let current = header_str
674 .get_or_insert_with(|| String::from_utf8_lossy(header_bytes).into_owned());
675 *current = secret.substitute_in_headers(current);
676 }
677
678 if body_substitution_allowed && secret.inject_body {
680 let source = body.as_deref().unwrap_or(body_bytes);
681 if let Some(replaced) = replace_bytes(
682 source,
683 secret.placeholder.as_bytes(),
684 secret.value.as_bytes(),
685 ) {
686 body = Some(replaced);
687 }
688 }
689 }
690
691 let header_changed = header_str
692 .as_ref()
693 .is_some_and(|headers| headers.as_bytes() != header_bytes);
694 let body_changed = body.is_some();
695
696 if !header_changed && !body_changed {
699 return self.append_pipelined_spillover(data, this_request, spillover);
700 }
701
702 let header_len = header_str
703 .as_ref()
704 .map_or(header_bytes.len(), |headers| headers.len());
705 let body_len = body.as_ref().map_or(body_bytes.len(), Vec::len);
706 let mut output = Vec::with_capacity(header_len + body_len + spillover.len());
707
708 let body_bytes_out = body.as_deref().unwrap_or(body_bytes);
709 if body_changed && body_bytes_out.len() != body_bytes.len() {
711 let headers = match header_str {
712 Some(headers) => update_content_length(&headers, body_bytes_out.len()),
713 None => update_content_length(
714 String::from_utf8_lossy(header_bytes).as_ref(),
715 body_bytes_out.len(),
716 ),
717 };
718 output.extend_from_slice(headers.as_bytes());
719 } else if let Some(headers) = header_str {
720 output.extend_from_slice(headers.as_bytes());
721 } else {
722 output.extend_from_slice(header_bytes);
723 }
724
725 output.extend_from_slice(body_bytes_out);
726
727 if !spillover.is_empty() {
728 let next_out = self.substitute(spillover)?;
729 output.extend_from_slice(next_out.as_ref());
730 }
731 Ok(Cow::Owned(output))
732 }
733
734 fn substitute_buffered_body<'a>(
735 &mut self,
736 data: &'a [u8],
737 remaining: usize,
738 ) -> Result<Cow<'a, [u8]>, ViolationAction> {
739 let take = remaining.min(data.len());
740 self.http_pending.extend_from_slice(&data[..take]);
741
742 if take < remaining {
743 self.http_state = HttpState::BufferingBody {
744 remaining: remaining - take,
745 };
746 return Ok(Cow::Owned(Vec::new()));
747 }
748
749 self.http_state = HttpState::AwaitingHeaders;
750 let request = std::mem::take(&mut self.http_pending);
751 let mut output = self.substitute_ready(&request)?.into_owned();
752
753 if data.len() > take {
754 let spillover = self.substitute(&data[take..])?;
755 output.extend_from_slice(spillover.as_ref());
756 }
757
758 Ok(Cow::Owned(output))
759 }
760
761 fn append_pipelined_spillover<'a>(
766 &mut self,
767 parent: &'a [u8],
768 this_request: &'a [u8],
769 spillover: &'a [u8],
770 ) -> Result<Cow<'a, [u8]>, ViolationAction> {
771 if spillover.is_empty() {
772 return Ok(Cow::Borrowed(parent));
773 }
774 let next_out = self.substitute(spillover)?;
775 if let Cow::Borrowed(b) = &next_out
776 && std::ptr::eq(b.as_ptr(), spillover.as_ptr())
777 && b.len() == spillover.len()
778 {
779 return Ok(Cow::Borrowed(parent));
783 }
784 let next_bytes = next_out.as_ref();
785 let mut out = Vec::with_capacity(this_request.len() + next_bytes.len());
786 out.extend_from_slice(this_request);
787 out.extend_from_slice(next_bytes);
788 Ok(Cow::Owned(out))
789 }
790
791 fn substitute_chunked_ready<'a>(
793 &mut self,
794 parent: &'a [u8],
795 header_bytes: &'a [u8],
796 after_headers: &'a [u8],
797 headers: &str,
798 ) -> Result<Cow<'a, [u8]>, ViolationAction> {
799 if self.needs_body_injection() && !has_non_identity_content_encoding(headers) {
800 return self.substitute_chunked_rewrite_ready(
801 parent,
802 header_bytes,
803 after_headers,
804 headers,
805 );
806 }
807
808 let mut state = ChunkedBodyState::default();
809 let body_end =
810 self.consume_chunked_body_with_violation_detection(&mut state, after_headers)?;
811 let (body_part, spillover) = match body_end {
812 Some(end) => after_headers.split_at(end),
813 None => (after_headers, &[] as &[u8]),
814 };
815 let this_request = &parent[..header_bytes.len() + body_part.len()];
816
817 self.apply_blocking_action(self.detect_blocking_action(this_request, headers))?;
818 self.update_tail(this_request);
819
820 self.http_state = if body_end.is_some() {
821 HttpState::AwaitingHeaders
822 } else {
823 HttpState::InChunkedBody { state }
824 };
825
826 if let Some(headers) = self.substitute_header_bytes(header_bytes) {
827 let mut output = Vec::with_capacity(headers.len() + body_part.len() + spillover.len());
828 output.extend_from_slice(headers.as_bytes());
829 output.extend_from_slice(body_part);
830 if !spillover.is_empty() {
831 let next_out = self.substitute(spillover)?;
832 output.extend_from_slice(next_out.as_ref());
833 }
834 return Ok(Cow::Owned(output));
835 }
836
837 self.append_pipelined_spillover(parent, this_request, spillover)
838 }
839
840 fn substitute_chunked_rewrite_ready<'a>(
842 &mut self,
843 parent: &'a [u8],
844 header_bytes: &'a [u8],
845 after_headers: &'a [u8],
846 headers: &str,
847 ) -> Result<Cow<'a, [u8]>, ViolationAction> {
848 let mut state = ChunkedRewriteState::default();
849 let rewrite = self.rewrite_chunked_body_part(&mut state, after_headers)?;
850 let (body_part, spillover) = match rewrite.body_end {
851 Some(end) => after_headers.split_at(end),
852 None => (after_headers, &[] as &[u8]),
853 };
854 let this_request = &parent[..header_bytes.len() + body_part.len()];
855
856 self.apply_blocking_action(self.detect_blocking_action(this_request, headers))?;
857 self.update_tail(this_request);
858
859 self.http_state = if rewrite.body_end.is_some() {
860 HttpState::AwaitingHeaders
861 } else {
862 HttpState::InChunkedRewriteBody { state }
863 };
864
865 let header_len = header_bytes.len();
866 let header_out = self.substitute_header_bytes(header_bytes);
867 let mut output = Vec::with_capacity(
868 header_out
869 .as_ref()
870 .map_or(header_len, |headers| headers.len())
871 + rewrite.output.len()
872 + spillover.len(),
873 );
874 if let Some(headers) = header_out {
875 output.extend_from_slice(headers.as_bytes());
876 } else {
877 output.extend_from_slice(header_bytes);
878 }
879 output.extend_from_slice(&rewrite.output);
880
881 if !spillover.is_empty() {
882 let next_out = self.substitute(spillover)?;
883 output.extend_from_slice(next_out.as_ref());
884 }
885
886 Ok(Cow::Owned(output))
887 }
888
889 fn substitute_body_chunk<'a>(
899 &mut self,
900 data: &'a [u8],
901 remaining: usize,
902 ) -> Result<Cow<'a, [u8]>, ViolationAction> {
903 let body_end = (data.len() >= remaining).then_some(remaining);
908 let (body_part, spillover) = match body_end {
909 Some(end) => data.split_at(end),
910 None => (data, &[] as &[u8]),
911 };
912
913 if let Some(action) = self.detect_blocking_action(body_part, "") {
914 match action {
915 BlockingAction::Block => return Err(action.into_violation_action()),
916 BlockingAction::BlockAndLog => {
917 tracing::warn!("secret violation: placeholder detected for disallowed host");
918 return Err(action.into_violation_action());
919 }
920 BlockingAction::BlockAndTerminate => {
921 tracing::error!(
922 "secret violation: placeholder detected for disallowed host - terminating"
923 );
924 return Err(action.into_violation_action());
925 }
926 }
927 }
928 self.update_tail(body_part);
929
930 self.http_state = match body_end {
933 Some(_) => HttpState::AwaitingHeaders,
934 None => HttpState::InBody {
935 remaining: remaining - body_part.len(),
936 },
937 };
938
939 self.append_pipelined_spillover(data, body_part, spillover)
940 }
941
942 fn substitute_chunked_body_chunk<'a>(
944 &mut self,
945 data: &'a [u8],
946 mut state: ChunkedBodyState,
947 ) -> Result<Cow<'a, [u8]>, ViolationAction> {
948 let body_end = self.consume_chunked_body_with_violation_detection(&mut state, data)?;
949 let (body_part, spillover) = match body_end {
950 Some(end) => data.split_at(end),
951 None => (data, &[] as &[u8]),
952 };
953
954 if let Some(action) = self.detect_blocking_action(body_part, "") {
955 match action {
956 BlockingAction::Block => return Err(action.into_violation_action()),
957 BlockingAction::BlockAndLog => {
958 tracing::warn!("secret violation: placeholder detected for disallowed host");
959 return Err(action.into_violation_action());
960 }
961 BlockingAction::BlockAndTerminate => {
962 tracing::error!(
963 "secret violation: placeholder detected for disallowed host - terminating"
964 );
965 return Err(action.into_violation_action());
966 }
967 }
968 }
969 self.update_tail(body_part);
970
971 self.http_state = if body_end.is_some() {
972 HttpState::AwaitingHeaders
973 } else {
974 HttpState::InChunkedBody { state }
975 };
976
977 self.append_pipelined_spillover(data, body_part, spillover)
978 }
979
980 fn substitute_chunked_rewrite_body_chunk<'a>(
983 &mut self,
984 data: &'a [u8],
985 mut state: ChunkedRewriteState,
986 ) -> Result<Cow<'a, [u8]>, ViolationAction> {
987 let rewrite = self.rewrite_chunked_body_part(&mut state, data)?;
988 let (body_part, spillover) = match rewrite.body_end {
989 Some(end) => data.split_at(end),
990 None => (data, &[] as &[u8]),
991 };
992
993 self.apply_blocking_action(self.detect_blocking_action(body_part, ""))?;
994 self.update_tail(body_part);
995
996 self.http_state = if rewrite.body_end.is_some() {
997 HttpState::AwaitingHeaders
998 } else {
999 HttpState::InChunkedRewriteBody { state }
1000 };
1001
1002 let mut output = rewrite.output;
1003 if !spillover.is_empty() {
1004 let next_out = self.substitute(spillover)?;
1005 output.extend_from_slice(next_out.as_ref());
1006 }
1007
1008 Ok(Cow::Owned(output))
1009 }
1010
1011 pub fn is_empty(&self) -> bool {
1013 self.http_sni.is_none()
1014 && self.http_pending.is_empty()
1015 && self.http2_state.is_none()
1016 && matches!(self.http_state, HttpState::AwaitingHeaders)
1017 && self.eligible_for_substitution.is_empty()
1018 && self.ineligible_for_substitution.is_empty()
1019 }
1020
1021 fn needs_body_injection(&self) -> bool {
1022 self.eligible_for_substitution.iter().any(|secret| {
1023 secret.inject_body && (!secret.require_tls_identity || self.tls_intercepted)
1024 })
1025 }
1026
1027 fn contains_eligible_http2_body_placeholder(&self, prev_tail: &[u8], data: &[u8]) -> bool {
1028 if !self.needs_body_injection() {
1029 return false;
1030 }
1031
1032 let scan_buf: Cow<[u8]> = if prev_tail.is_empty() {
1033 Cow::Borrowed(data)
1034 } else {
1035 let mut stitched = Vec::with_capacity(prev_tail.len() + data.len());
1036 stitched.extend_from_slice(prev_tail);
1037 stitched.extend_from_slice(data);
1038 Cow::Owned(stitched)
1039 };
1040 let scan = scan_buf.as_ref();
1041 self.eligible_for_substitution.iter().any(|secret| {
1042 secret.inject_body
1043 && !secret.placeholder.is_empty()
1044 && (!secret.require_tls_identity || self.tls_intercepted)
1045 && contains_bytes(scan, secret.placeholder.as_bytes())
1046 })
1047 }
1048
1049 fn substitute_http2_headers(&self, headers: &mut [(Vec<u8>, Vec<u8>)]) {
1050 for secret in &self.eligible_for_substitution {
1051 if secret.require_tls_identity && !self.tls_intercepted {
1052 continue;
1053 }
1054
1055 for (name, value) in headers.iter_mut() {
1056 let is_pseudo = name.starts_with(b":");
1057
1058 if name.eq_ignore_ascii_case(b":path")
1059 && secret.inject_query_params
1060 && let Ok(path) = std::str::from_utf8(value)
1061 && let Some(replaced) =
1062 substitute_query_in_target(path, &secret.placeholder, &secret.value)
1063 {
1064 *value = replaced.into_bytes();
1065 }
1066
1067 if !is_pseudo
1068 && name.eq_ignore_ascii_case(b"authorization")
1069 && secret.inject_basic_auth
1070 && let Ok(header_value) = std::str::from_utf8(value)
1071 && let Some(replaced) = substitute_basic_auth_value(
1072 header_value,
1073 &secret.placeholder,
1074 &secret.value,
1075 )
1076 {
1077 *value = replaced.into_bytes();
1078 }
1079
1080 if !is_pseudo
1081 && secret.inject_headers
1082 && contains_bytes(value, secret.placeholder.as_bytes())
1083 {
1084 let replaced =
1085 String::from_utf8_lossy(value).replace(&secret.placeholder, &secret.value);
1086 *value = replaced.into_bytes();
1087 }
1088 }
1089 }
1090 }
1091
1092 fn substitute_header_bytes(&self, header_bytes: &[u8]) -> Option<String> {
1093 let mut header_str: Option<String> = None;
1094 for secret in &self.eligible_for_substitution {
1095 if secret.require_tls_identity && !self.tls_intercepted {
1096 continue;
1097 }
1098 if secret.may_substitute_in_headers(header_bytes) {
1099 let current = header_str
1100 .get_or_insert_with(|| String::from_utf8_lossy(header_bytes).into_owned());
1101 *current = secret.substitute_in_headers(current);
1102 }
1103 }
1104
1105 header_str.filter(|headers| headers.as_bytes() != header_bytes)
1106 }
1107
1108 fn consume_chunked_body_with_violation_detection(
1109 &self,
1110 state: &mut ChunkedBodyState,
1111 data: &[u8],
1112 ) -> Result<Option<usize>, ViolationAction> {
1113 let mut decoded_tail = std::mem::take(&mut state.decoded_tail);
1114 let body_end = process_chunked_body(state, data, |event| {
1115 let ChunkedBodyEvent::Payload(payload) = event else {
1116 return Ok(());
1117 };
1118 self.apply_blocking_action(detect_blocking_action_with_tail(
1119 &self.ineligible_for_substitution,
1120 &decoded_tail,
1121 payload,
1122 "",
1123 ))?;
1124 update_tail_buffer(
1125 &mut decoded_tail,
1126 payload,
1127 self.max_detection_window_len.saturating_sub(1),
1128 );
1129 Ok(())
1130 });
1131 state.decoded_tail = decoded_tail;
1132 body_end
1133 }
1134
1135 fn rewrite_chunked_body_part(
1136 &self,
1137 state: &mut ChunkedRewriteState,
1138 data: &[u8],
1139 ) -> Result<ChunkedRewriteResult, ViolationAction> {
1140 let mut output = Vec::new();
1141 let mut decoded_tail = std::mem::take(&mut state.parser.decoded_tail);
1142 let mut substitution_tail = std::mem::take(&mut state.substitution_tail);
1143
1144 let body_end = process_chunked_body(&mut state.parser, data, |event| {
1145 match event {
1146 ChunkedBodyEvent::Payload(payload) => {
1147 self.apply_blocking_action(detect_blocking_action_with_tail(
1148 &self.ineligible_for_substitution,
1149 &decoded_tail,
1150 payload,
1151 "",
1152 ))?;
1153 update_tail_buffer(
1154 &mut decoded_tail,
1155 payload,
1156 self.max_detection_window_len.saturating_sub(1),
1157 );
1158 self.append_rewritten_chunked_payload(
1159 &mut substitution_tail,
1160 payload,
1161 &mut output,
1162 );
1163 }
1164 ChunkedBodyEvent::ZeroChunk => {
1165 self.flush_rewritten_chunked_payload(&mut substitution_tail, &mut output);
1166 output.extend_from_slice(b"0\r\n");
1167 }
1168 ChunkedBodyEvent::TrailerLine(trailer_line) => {
1169 output.extend_from_slice(trailer_line);
1170 }
1171 }
1172 Ok(())
1173 })?;
1174
1175 state.parser.decoded_tail = decoded_tail;
1176 state.substitution_tail = substitution_tail;
1177
1178 Ok(ChunkedRewriteResult { output, body_end })
1179 }
1180
1181 fn append_rewritten_chunked_payload(
1182 &self,
1183 substitution_tail: &mut Vec<u8>,
1184 payload: &[u8],
1185 output: &mut Vec<u8>,
1186 ) {
1187 substitution_tail.extend_from_slice(payload);
1188 let carry_len = self.max_body_placeholder_len.saturating_sub(1);
1189 self.append_rewritten_chunked_prefix(substitution_tail, carry_len, output);
1190 }
1191
1192 fn flush_rewritten_chunked_payload(
1193 &self,
1194 substitution_tail: &mut Vec<u8>,
1195 output: &mut Vec<u8>,
1196 ) {
1197 self.append_rewritten_chunked_prefix(substitution_tail, 0, output);
1198 }
1199
1200 fn append_rewritten_chunked_prefix(
1201 &self,
1202 substitution_tail: &mut Vec<u8>,
1203 keep_len: usize,
1204 output: &mut Vec<u8>,
1205 ) {
1206 let safe_len = substitution_tail.len().saturating_sub(keep_len);
1207 if safe_len == 0 {
1208 return;
1209 }
1210
1211 let mut cursor = 0;
1212 let mut chunk_payload = Vec::with_capacity(safe_len);
1213 while cursor < safe_len {
1214 if let Some(secret) = self.matching_body_secret_at(&substitution_tail[cursor..]) {
1215 chunk_payload.extend_from_slice(secret.value.as_bytes());
1216 cursor += secret.placeholder.len();
1217 } else {
1218 chunk_payload.push(substitution_tail[cursor]);
1219 cursor += 1;
1220 }
1221 }
1222
1223 let kept = substitution_tail.split_off(cursor);
1224 *substitution_tail = kept;
1225 append_chunk(output, &chunk_payload);
1226 }
1227
1228 fn matching_body_secret_at(&self, data: &[u8]) -> Option<&EligibleSecret> {
1229 self.eligible_for_substitution.iter().find(|secret| {
1230 secret.inject_body
1231 && !secret.placeholder.is_empty()
1232 && (!secret.require_tls_identity || self.tls_intercepted)
1233 && data.starts_with(secret.placeholder.as_bytes())
1234 })
1235 }
1236
1237 fn apply_blocking_action(&self, action: Option<BlockingAction>) -> Result<(), ViolationAction> {
1238 match action {
1239 Some(BlockingAction::Block) => Err(ViolationAction::Block),
1240 Some(BlockingAction::BlockAndLog) => {
1241 tracing::warn!("secret violation: placeholder detected for disallowed host");
1242 Err(ViolationAction::BlockAndLog)
1243 }
1244 Some(BlockingAction::BlockAndTerminate) => {
1245 tracing::error!(
1246 "secret violation: placeholder detected for disallowed host - terminating"
1247 );
1248 Err(ViolationAction::BlockAndTerminate)
1249 }
1250 None => Ok(()),
1251 }
1252 }
1253
1254 fn detect_blocking_action(&self, data: &[u8], headers: &str) -> Option<BlockingAction> {
1262 detect_blocking_action_with_tail(
1263 &self.ineligible_for_substitution,
1264 &self.prev_tail,
1265 data,
1266 headers,
1267 )
1268 }
1269
1270 fn update_tail(&mut self, data: &[u8]) {
1274 update_tail_buffer(
1275 &mut self.prev_tail,
1276 data,
1277 self.max_detection_window_len.saturating_sub(1),
1278 );
1279 }
1280}
1281
1282impl Http2State {
1283 fn process(
1284 &mut self,
1285 handler: &mut SecretsHandler,
1286 data: &[u8],
1287 ) -> Result<Vec<u8>, ViolationAction> {
1288 self.buffer.extend_from_slice(data);
1289 let mut output = Vec::new();
1290
1291 if !self.preface_seen {
1292 if self.buffer.len() < HTTP2_PREFACE.len() {
1293 return Ok(output);
1294 }
1295 if !self.buffer.starts_with(HTTP2_PREFACE) {
1296 return Err(ViolationAction::Block);
1297 }
1298 output.extend_from_slice(HTTP2_PREFACE);
1299 self.buffer.drain(..HTTP2_PREFACE.len());
1300 self.preface_seen = true;
1301 }
1302
1303 loop {
1304 if self.buffer.len() < 9 {
1305 break;
1306 }
1307
1308 let frame_len = http2_frame_payload_len(&self.buffer[..9]);
1309 if frame_len > MAX_HTTP2_FRAME_PAYLOAD_BYTES {
1310 return Err(ViolationAction::Block);
1311 }
1312 let full_len = 9 + frame_len;
1313 if self.buffer.len() < full_len {
1314 break;
1315 }
1316
1317 let frame = self.buffer[..full_len].to_vec();
1318 self.buffer.drain(..full_len);
1319 self.process_frame(handler, &frame, &mut output)?;
1320 }
1321
1322 Ok(output)
1323 }
1324
1325 fn process_frame(
1326 &mut self,
1327 handler: &mut SecretsHandler,
1328 raw: &[u8],
1329 output: &mut Vec<u8>,
1330 ) -> Result<(), ViolationAction> {
1331 let frame = parse_http2_frame(raw)?;
1332
1333 if self.header_block.is_some() && frame.kind != HTTP2_FRAME_CONTINUATION {
1334 return Err(ViolationAction::Block);
1335 }
1336
1337 match frame.kind {
1338 HTTP2_FRAME_HEADERS => self.process_headers_frame(handler, frame, output),
1339 HTTP2_FRAME_CONTINUATION => self.process_continuation_frame(handler, frame, output),
1340 HTTP2_FRAME_DATA => self.process_data_frame(handler, frame, output),
1341 HTTP2_FRAME_PUSH_PROMISE => Err(ViolationAction::Block),
1342 _ => {
1343 output.extend_from_slice(frame.raw);
1344 Ok(())
1345 }
1346 }
1347 }
1348
1349 fn process_headers_frame(
1350 &mut self,
1351 handler: &mut SecretsHandler,
1352 frame: Http2Frame<'_>,
1353 output: &mut Vec<u8>,
1354 ) -> Result<(), ViolationAction> {
1355 if frame.stream_id == 0 || frame.stream_id.is_multiple_of(2) || self.header_block.is_some()
1356 {
1357 return Err(ViolationAction::Block);
1358 }
1359
1360 let fragment = http2_headers_fragment(frame.flags, frame.payload)?;
1361 if fragment.len() > MAX_HTTP2_HEADER_BLOCK_BYTES {
1362 return Err(ViolationAction::Block);
1363 }
1364
1365 let block = Http2HeaderBlock {
1366 stream_id: frame.stream_id,
1367 end_stream: frame.flags & HTTP2_FLAG_END_STREAM != 0,
1368 block: fragment.to_vec(),
1369 };
1370
1371 if frame.flags & HTTP2_FLAG_END_HEADERS != 0 {
1372 self.finish_header_block(handler, block, output)
1373 } else {
1374 self.header_block = Some(block);
1375 Ok(())
1376 }
1377 }
1378
1379 fn process_continuation_frame(
1380 &mut self,
1381 handler: &mut SecretsHandler,
1382 frame: Http2Frame<'_>,
1383 output: &mut Vec<u8>,
1384 ) -> Result<(), ViolationAction> {
1385 let Some(mut block) = self.header_block.take() else {
1386 return Err(ViolationAction::Block);
1387 };
1388 if frame.stream_id == 0 || frame.stream_id != block.stream_id {
1389 return Err(ViolationAction::Block);
1390 }
1391
1392 block.block.extend_from_slice(frame.payload);
1393 if block.block.len() > MAX_HTTP2_HEADER_BLOCK_BYTES {
1394 return Err(ViolationAction::Block);
1395 }
1396
1397 if frame.flags & HTTP2_FLAG_END_HEADERS != 0 {
1398 self.finish_header_block(handler, block, output)
1399 } else {
1400 self.header_block = Some(block);
1401 Ok(())
1402 }
1403 }
1404
1405 fn process_data_frame(
1406 &mut self,
1407 handler: &mut SecretsHandler,
1408 frame: Http2Frame<'_>,
1409 output: &mut Vec<u8>,
1410 ) -> Result<(), ViolationAction> {
1411 if frame.stream_id == 0 || !self.open_request_streams.contains(&frame.stream_id) {
1412 return Err(ViolationAction::Block);
1413 }
1414
1415 let data = http2_data_payload(frame.flags, frame.payload)?;
1416 let tail = self.data_tails.entry(frame.stream_id).or_default();
1417 if handler.contains_eligible_http2_body_placeholder(tail, data) {
1418 tracing::warn!(
1419 "secret substitution in HTTP/2 DATA frames is unsupported; blocking placeholder"
1420 );
1421 return Err(ViolationAction::Block);
1422 }
1423 handler.apply_blocking_action(detect_blocking_action_with_tail(
1424 &handler.ineligible_for_substitution,
1425 tail,
1426 data,
1427 "",
1428 ))?;
1429 update_tail_buffer(
1430 tail,
1431 data,
1432 handler.max_detection_window_len.saturating_sub(1),
1433 );
1434 if frame.flags & HTTP2_FLAG_END_STREAM != 0 {
1435 self.data_tails.remove(&frame.stream_id);
1436 self.open_request_streams.remove(&frame.stream_id);
1437 }
1438 output.extend_from_slice(frame.raw);
1439 Ok(())
1440 }
1441
1442 fn finish_header_block(
1443 &mut self,
1444 handler: &mut SecretsHandler,
1445 block: Http2HeaderBlock,
1446 output: &mut Vec<u8>,
1447 ) -> Result<(), ViolationAction> {
1448 let mut headers = self.decode_headers(&block.block)?;
1449 let is_initial_request = !self.open_request_streams.contains(&block.stream_id);
1450 if is_initial_request {
1451 if self.open_request_streams.len() >= MAX_HTTP2_TRACKED_STREAMS {
1452 return Err(ViolationAction::Block);
1453 }
1454 self.open_request_streams.insert(block.stream_id);
1455 } else if !block.end_stream {
1456 return Err(ViolationAction::Block);
1457 }
1458
1459 if let Some(sni) = handler.http_sni.as_deref() {
1460 validate_http2_authority(&headers, sni, is_initial_request)?;
1461 }
1462
1463 let detection_bytes = http2_header_detection_bytes(&headers);
1464 let detection_text = String::from_utf8_lossy(&detection_bytes);
1465 handler.apply_blocking_action(detect_blocking_action_with_tail(
1466 &handler.ineligible_for_substitution,
1467 &[],
1468 &detection_bytes,
1469 detection_text.as_ref(),
1470 ))?;
1471
1472 handler.substitute_http2_headers(&mut headers);
1473 let encoded = self.encode_headers(&headers)?;
1474 append_http2_header_frames(output, block.stream_id, block.end_stream, &encoded)?;
1475 if block.end_stream {
1476 self.data_tails.remove(&block.stream_id);
1477 self.open_request_streams.remove(&block.stream_id);
1478 }
1479 Ok(())
1480 }
1481
1482 fn decode_headers(&mut self, block: &[u8]) -> Result<Http2Headers, ViolationAction> {
1483 let mut block = block.to_vec();
1484 let mut headers = Vec::new();
1485 let mut decoded_bytes = 0usize;
1486
1487 while !block.is_empty() {
1488 let before_len = block.len();
1489 let mut decoded = Vec::with_capacity(1);
1490 self.decoder
1491 .decode_exact(&mut block, &mut decoded)
1492 .map_err(|_| ViolationAction::Block)?;
1493 if decoded.is_empty() {
1494 if block.len() == before_len {
1495 return Err(ViolationAction::Block);
1496 }
1497 continue;
1498 }
1499
1500 if headers.len() >= MAX_HTTP2_HEADER_FIELDS {
1501 return Err(ViolationAction::Block);
1502 }
1503 let (name, value, _flags) = decoded.pop().expect("decoded one header");
1504 decoded_bytes = decoded_bytes
1505 .checked_add(name.len())
1506 .and_then(|len| len.checked_add(value.len()))
1507 .and_then(|len| len.checked_add(4))
1508 .ok_or(ViolationAction::Block)?;
1509 if decoded_bytes > MAX_HTTP2_DECODED_HEADER_BYTES {
1510 return Err(ViolationAction::Block);
1511 }
1512
1513 headers.push((name, value));
1514 }
1515
1516 Ok(headers)
1517 }
1518
1519 fn encode_headers(
1520 &mut self,
1521 headers: &[(Vec<u8>, Vec<u8>)],
1522 ) -> Result<Vec<u8>, ViolationAction> {
1523 let mut encoded = Vec::new();
1524 for (name, value) in headers {
1525 self.encoder
1526 .encode(
1527 (name.clone(), value.clone(), HpackEncoder::NEVER_INDEXED),
1528 &mut encoded,
1529 )
1530 .map_err(|_| ViolationAction::Block)?;
1531 }
1532 Ok(encoded)
1533 }
1534}
1535
1536fn is_authorization_header(line: &str) -> bool {
1543 line.as_bytes()
1544 .get(..14)
1545 .is_some_and(|b| b.eq_ignore_ascii_case(b"authorization:"))
1546}
1547
1548fn is_http2_preface_prefix(data: &[u8]) -> bool {
1549 !data.is_empty()
1550 && if data.len() <= HTTP2_PREFACE.len() {
1551 HTTP2_PREFACE.starts_with(data)
1552 } else {
1553 data.starts_with(HTTP2_PREFACE)
1554 }
1555}
1556
1557fn has_complete_http2_preface(data: &[u8]) -> bool {
1558 data.len() >= HTTP2_PREFACE.len() && data.starts_with(HTTP2_PREFACE)
1559}
1560
1561fn http2_frame_payload_len(header: &[u8]) -> usize {
1562 ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize
1563}
1564
1565fn parse_http2_frame(raw: &[u8]) -> Result<Http2Frame<'_>, ViolationAction> {
1566 if raw.len() < 9 {
1567 return Err(ViolationAction::Block);
1568 }
1569 let len = http2_frame_payload_len(raw);
1570 if raw.len() != 9 + len {
1571 return Err(ViolationAction::Block);
1572 }
1573
1574 let stream_id = u32::from_be_bytes([raw[5], raw[6], raw[7], raw[8]]) & 0x7fff_ffff;
1575 Ok(Http2Frame {
1576 kind: raw[3],
1577 flags: raw[4],
1578 stream_id,
1579 payload: &raw[9..],
1580 raw,
1581 })
1582}
1583
1584fn http2_headers_fragment(flags: u8, payload: &[u8]) -> Result<&[u8], ViolationAction> {
1585 let mut start = 0;
1586 let pad_len = if flags & HTTP2_FLAG_PADDED != 0 {
1587 let Some(pad_len) = payload.first() else {
1588 return Err(ViolationAction::Block);
1589 };
1590 start = 1;
1591 *pad_len as usize
1592 } else {
1593 0
1594 };
1595
1596 if flags & HTTP2_FLAG_PRIORITY != 0 {
1597 start += 5;
1598 }
1599 if payload.len() < start + pad_len {
1600 return Err(ViolationAction::Block);
1601 }
1602
1603 Ok(&payload[start..payload.len() - pad_len])
1604}
1605
1606fn http2_data_payload(flags: u8, payload: &[u8]) -> Result<&[u8], ViolationAction> {
1607 if flags & HTTP2_FLAG_PADDED == 0 {
1608 return Ok(payload);
1609 }
1610
1611 let Some(pad_len) = payload.first() else {
1612 return Err(ViolationAction::Block);
1613 };
1614 let pad_len = *pad_len as usize;
1615 if payload.len() < 1 + pad_len {
1616 return Err(ViolationAction::Block);
1617 }
1618
1619 Ok(&payload[1..payload.len() - pad_len])
1620}
1621
1622fn append_http2_header_frames(
1623 output: &mut Vec<u8>,
1624 stream_id: u32,
1625 end_stream: bool,
1626 block: &[u8],
1627) -> Result<(), ViolationAction> {
1628 let mut first = true;
1629 let mut offset = 0;
1630
1631 while first || offset < block.len() {
1632 let remaining = block.len().saturating_sub(offset);
1633 let take = remaining.min(HTTP2_OUTBOUND_FRAME_PAYLOAD_BYTES);
1634 let payload = &block[offset..offset + take];
1635 offset += take;
1636
1637 let kind = if first {
1638 HTTP2_FRAME_HEADERS
1639 } else {
1640 HTTP2_FRAME_CONTINUATION
1641 };
1642 let mut flags = 0;
1643 if offset == block.len() {
1644 flags |= HTTP2_FLAG_END_HEADERS;
1645 }
1646 if first && end_stream {
1647 flags |= HTTP2_FLAG_END_STREAM;
1648 }
1649
1650 append_http2_frame(output, kind, flags, stream_id, payload)?;
1651 first = false;
1652 }
1653
1654 Ok(())
1655}
1656
1657fn append_http2_frame(
1658 output: &mut Vec<u8>,
1659 kind: u8,
1660 flags: u8,
1661 stream_id: u32,
1662 payload: &[u8],
1663) -> Result<(), ViolationAction> {
1664 if payload.len() > 0x00ff_ffff || stream_id & 0x8000_0000 != 0 {
1665 return Err(ViolationAction::Block);
1666 }
1667
1668 output.push(((payload.len() >> 16) & 0xff) as u8);
1669 output.push(((payload.len() >> 8) & 0xff) as u8);
1670 output.push((payload.len() & 0xff) as u8);
1671 output.push(kind);
1672 output.push(flags);
1673 output.extend_from_slice(&stream_id.to_be_bytes());
1674 output.extend_from_slice(payload);
1675 Ok(())
1676}
1677
1678fn validate_http2_authority(
1679 headers: &[(Vec<u8>, Vec<u8>)],
1680 sni: &str,
1681 require_authority: bool,
1682) -> Result<(), ViolationAction> {
1683 let mut authority_count = 0usize;
1684
1685 for (name, value) in headers {
1686 if name.eq_ignore_ascii_case(b":authority") {
1687 authority_count += 1;
1688 let authority = String::from_utf8_lossy(value);
1689 if !authority_matches_sni(authority.as_ref(), sni) {
1690 return Err(ViolationAction::Block);
1691 }
1692 } else if name.eq_ignore_ascii_case(b"host") {
1693 let host = String::from_utf8_lossy(value);
1694 if !authority_matches_sni(host.as_ref(), sni) {
1695 return Err(ViolationAction::Block);
1696 }
1697 }
1698 }
1699
1700 if require_authority && authority_count != 1 {
1701 return Err(ViolationAction::Block);
1702 }
1703
1704 Ok(())
1705}
1706
1707fn http2_header_detection_bytes(headers: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
1708 let len = headers
1709 .iter()
1710 .map(|(name, value)| name.len() + value.len() + 4)
1711 .sum();
1712 let mut out = Vec::with_capacity(len);
1713 for (name, value) in headers {
1714 out.extend_from_slice(name);
1715 out.extend_from_slice(b": ");
1716 out.extend_from_slice(value);
1717 out.extend_from_slice(b"\r\n");
1718 }
1719 out
1720}
1721
1722fn parse_http_request_metadata(
1723 header_bytes: &[u8],
1724) -> Result<Option<HttpRequestMetadata>, ViolationAction> {
1725 let headers = String::from_utf8_lossy(header_bytes);
1726 let mut lines = headers.split("\r\n");
1727 let Some(request_line) = lines.next() else {
1728 return Ok(None);
1729 };
1730 if request_line.is_empty() {
1731 return Ok(None);
1732 }
1733
1734 let Some(version) = http_request_version(request_line) else {
1735 return Ok(None);
1736 };
1737 if version == "HTTP/2.0" {
1738 return Err(ViolationAction::Block);
1739 }
1740 if !version.starts_with("HTTP/1.") {
1741 return Ok(None);
1742 }
1743
1744 let mut host_headers = Vec::new();
1745 for line in lines.take_while(|line| !line.is_empty()) {
1746 let Some((name, value)) = line.split_once(':') else {
1747 continue;
1748 };
1749 let value = value.trim();
1750
1751 if name.eq_ignore_ascii_case("host") {
1752 host_headers.push(value.to_string());
1753 }
1754 }
1755
1756 if host_headers.is_empty() {
1757 return Err(ViolationAction::Block);
1758 }
1759
1760 Ok(Some(HttpRequestMetadata { host_headers }))
1761}
1762
1763fn http_request_version(request_line: &str) -> Option<&str> {
1764 let mut parts = request_line.split_whitespace();
1765 let method = parts.next()?;
1766 let _target = parts.next()?;
1767 let version = parts.next()?;
1768 if parts.next().is_some() || !method.bytes().all(is_http_token_byte) {
1769 return None;
1770 }
1771 Some(version)
1772}
1773
1774fn looks_like_http_request_prefix(data: &[u8]) -> bool {
1775 if data.is_empty() || b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".starts_with(data) {
1776 return true;
1777 }
1778
1779 let method_end = data.iter().position(|b| *b == b' ');
1780 let method = match method_end {
1781 Some(end) => &data[..end],
1782 None => data,
1783 };
1784
1785 !method.is_empty() && method.iter().copied().all(is_http_token_byte)
1786}
1787
1788fn first_line_is_not_http_request(data: &[u8]) -> bool {
1789 let Some(line_end) = data.windows(2).position(|window| window == b"\r\n") else {
1790 return false;
1791 };
1792 let line = String::from_utf8_lossy(&data[..line_end]);
1793 http_request_version(line.as_ref()).is_none()
1794}
1795
1796fn is_http_token_byte(byte: u8) -> bool {
1797 matches!(
1798 byte,
1799 b'!' | b'#'
1800 | b'$'
1801 | b'%'
1802 | b'&'
1803 | b'\''
1804 | b'*'
1805 | b'+'
1806 | b'-'
1807 | b'.'
1808 | b'^'
1809 | b'_'
1810 | b'`'
1811 | b'|'
1812 | b'~'
1813 | b'0'..=b'9'
1814 | b'A'..=b'Z'
1815 | b'a'..=b'z'
1816 )
1817}
1818
1819fn authority_matches_sni(authority: &str, sni: &str) -> bool {
1820 authority_hostname(authority)
1821 .is_some_and(|hostname| hostname.eq_ignore_ascii_case(sni.trim_end_matches('.')))
1822}
1823
1824fn authority_hostname(authority: &str) -> Option<&str> {
1825 let authority = authority.trim().trim_end_matches('.');
1826 if authority.is_empty() {
1827 return None;
1828 }
1829
1830 if let Some(rest) = authority.strip_prefix('[') {
1831 let (host, _port) = rest.split_once(']')?;
1832 return Some(host.trim_end_matches('.'));
1833 }
1834
1835 match authority.rsplit_once(':') {
1836 Some((host, port)) if !host.contains(':') && port.parse::<u16>().is_ok() => {
1837 Some(host.trim_end_matches('.'))
1838 }
1839 _ => Some(authority),
1840 }
1841}
1842
1843fn secret_host_allowed(
1844 secret: &SecretEntry,
1845 sni: &str,
1846 identity: Option<&SecretHostIdentity<'_>>,
1847) -> bool {
1848 secret
1849 .allowed_hosts
1850 .iter()
1851 .any(|pattern| host_pattern_allowed(pattern, sni, identity))
1852}
1853
1854fn host_pattern_allowed(
1855 pattern: &HostPattern,
1856 sni: &str,
1857 identity: Option<&SecretHostIdentity<'_>>,
1858) -> bool {
1859 if !pattern.matches(sni) {
1860 return false;
1861 }
1862 if matches!(pattern, HostPattern::Any) {
1863 return true;
1864 }
1865 let Some(identity) = identity else {
1866 return true;
1867 };
1868
1869 host_alias_matches(pattern, sni, identity)
1870 || identity
1871 .shared
1872 .any_resolved_hostname(identity.guest_ip, |hostname| pattern.matches(hostname))
1873}
1874
1875fn host_alias_matches(pattern: &HostPattern, sni: &str, identity: &SecretHostIdentity<'_>) -> bool {
1876 if !sni.eq_ignore_ascii_case(crate::HOST_ALIAS) || !pattern.matches(crate::HOST_ALIAS) {
1877 return false;
1878 }
1879
1880 identity
1881 .shared
1882 .gateway_ipv4()
1883 .is_some_and(|ip| identity.guest_ip == IpAddr::V4(ip))
1884 || identity
1885 .shared
1886 .gateway_ipv6()
1887 .is_some_and(|ip| identity.guest_ip == IpAddr::V6(ip))
1888}
1889
1890fn effective_violation_action<'a>(
1891 secret: &'a SecretEntry,
1892 config: &'a SecretsConfig,
1893 sni: &str,
1894 identity: Option<&SecretHostIdentity<'_>>,
1895) -> &'a ViolationAction {
1896 match &secret.on_violation {
1897 Some(ViolationAction::Passthrough(hosts))
1898 if !hosts
1899 .iter()
1900 .any(|pattern| host_pattern_allowed(pattern, sni, identity)) =>
1901 {
1902 &config.on_violation
1903 }
1904 Some(action) => action,
1905 None => &config.on_violation,
1906 }
1907}
1908
1909fn decode_basic_credentials(line: &str) -> Option<String> {
1913 let (_, raw_value) = line.split_once(':')?;
1914 let (scheme, encoded) = split_auth_scheme(raw_value.trim_start())?;
1915 if !scheme.eq_ignore_ascii_case("basic") {
1916 return None;
1917 }
1918 let bytes = BASE64.decode(encoded.trim()).ok()?;
1919 String::from_utf8(bytes).ok()
1920}
1921
1922fn split_auth_scheme(header_value: &str) -> Option<(&str, &str)> {
1925 let split_at = header_value.find(char::is_whitespace)?;
1926 let (scheme, rest) = header_value.split_at(split_at);
1927 Some((scheme, rest.trim_start()))
1928}
1929
1930fn substitute_query_in_request_line(line: &str, placeholder: &str, value: &str) -> Option<String> {
1931 if placeholder.is_empty() {
1932 return None;
1933 }
1934
1935 let method_end = line.find(' ')?;
1936 let target_start = method_end + 1;
1937 let version_start = line[target_start..].rfind(' ')? + target_start;
1938 if version_start <= target_start {
1939 return None;
1940 }
1941
1942 let target = &line[target_start..version_start];
1943 let query_start = target.find('?')? + 1;
1944 let query = &target[query_start..];
1945 if !query.contains(placeholder) {
1946 return None;
1947 }
1948
1949 let mut result = String::with_capacity(line.len());
1950 result.push_str(&line[..target_start + query_start]);
1951 result.push_str(&query.replace(placeholder, value));
1952 result.push_str(&line[version_start..]);
1953 Some(result)
1954}
1955
1956fn substitute_query_in_target(target: &str, placeholder: &str, value: &str) -> Option<String> {
1957 if placeholder.is_empty() {
1958 return None;
1959 }
1960
1961 let query_start = target.find('?')? + 1;
1962 let query = &target[query_start..];
1963 if !query.contains(placeholder) {
1964 return None;
1965 }
1966
1967 let mut result = String::with_capacity(target.len());
1968 result.push_str(&target[..query_start]);
1969 result.push_str(&query.replace(placeholder, value));
1970 Some(result)
1971}
1972
1973fn substitute_basic_auth_value(
1974 header_value: &str,
1975 placeholder: &str,
1976 value: &str,
1977) -> Option<String> {
1978 let (scheme, encoded) = split_auth_scheme(header_value.trim_start())?;
1979 if !scheme.eq_ignore_ascii_case("basic") {
1980 return None;
1981 }
1982 let bytes = BASE64.decode(encoded.trim()).ok()?;
1983 let decoded = String::from_utf8(bytes).ok()?;
1984 if !decoded.contains(placeholder) {
1985 return None;
1986 }
1987 let replaced = decoded.replace(placeholder, value);
1988 Some(format!("Basic {}", BASE64.encode(replaced.as_bytes())))
1989}
1990
1991fn basic_auth_decoded_contains(headers: &str, placeholder: &str) -> bool {
1994 decoded_basic_auth_credentials(headers)
1995 .iter()
1996 .any(|decoded| decoded.contains(placeholder))
1997}
1998
1999fn decoded_basic_auth_credentials(headers: &str) -> Vec<String> {
2001 headers
2002 .split("\r\n")
2003 .filter(|line| is_authorization_header(line))
2004 .filter_map(decode_basic_credentials)
2005 .collect()
2006}
2007
2008fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
2010 if needle.is_empty() || haystack.len() < needle.len() {
2011 return false;
2012 }
2013 haystack.windows(needle.len()).any(|w| w == needle)
2014}
2015
2016fn max_placeholder_detection_len(placeholder_len: usize) -> usize {
2020 placeholder_len.saturating_mul(6)
2021}
2022
2023fn next_state_after_headers(
2029 headers: &str,
2030 body_bytes: &[u8],
2031) -> Result<RequestFraming, ViolationAction> {
2032 let body_in_chunk = body_bytes.len();
2033 let body_substitution_allowed = !has_non_identity_content_encoding(headers);
2034 if is_transfer_chunked(headers) {
2035 let mut chunked_state = ChunkedBodyState::default();
2036 let (state, body_in_request) = match consume_chunked_body(&mut chunked_state, body_bytes)? {
2037 Some(end) => (HttpState::AwaitingHeaders, end),
2038 _ => (
2039 HttpState::InChunkedBody {
2040 state: chunked_state,
2041 },
2042 body_in_chunk,
2043 ),
2044 };
2045 return Ok(RequestFraming {
2046 state,
2047 body_in_request,
2048 body_substitution_allowed: false,
2049 });
2050 }
2051 match parse_content_length(headers)? {
2052 Some(cl) if body_in_chunk >= cl => Ok(RequestFraming {
2053 state: HttpState::AwaitingHeaders,
2054 body_in_request: cl,
2055 body_substitution_allowed,
2056 }),
2057 Some(cl) => Ok(RequestFraming {
2058 state: HttpState::InBody {
2059 remaining: cl - body_in_chunk,
2060 },
2061 body_in_request: body_in_chunk,
2062 body_substitution_allowed,
2063 }),
2064 None => Ok(RequestFraming {
2068 state: HttpState::AwaitingHeaders,
2069 body_in_request: 0,
2070 body_substitution_allowed: false,
2071 }),
2072 }
2073}
2074
2075fn parse_content_length(headers: &str) -> Result<Option<usize>, ViolationAction> {
2078 let mut content_length = None;
2079 for line in headers.split("\r\n") {
2080 let Some((name, value)) = line.split_once(':') else {
2081 continue;
2082 };
2083 if name.eq_ignore_ascii_case("content-length") {
2084 let parsed = value
2085 .trim()
2086 .parse::<usize>()
2087 .map_err(|_| ViolationAction::Block)?;
2088 if content_length.is_some_and(|existing| existing != parsed) {
2089 return Err(ViolationAction::Block);
2090 }
2091 content_length = Some(parsed);
2092 }
2093 }
2094 Ok(content_length)
2095}
2096
2097fn content_length_exceeds_buffer_limit(headers: &str) -> Result<bool, ViolationAction> {
2098 Ok(parse_content_length(headers)?.is_some_and(|len| len > MAX_HTTP_BODY_BUFFER_BYTES))
2099}
2100
2101fn is_transfer_chunked(headers: &str) -> bool {
2104 for line in headers.split("\r\n") {
2105 let Some((name, value)) = line.split_once(':') else {
2106 continue;
2107 };
2108 if name.eq_ignore_ascii_case("transfer-encoding")
2109 && value
2110 .split(',')
2111 .next_back()
2112 .map(|s| s.trim().eq_ignore_ascii_case("chunked"))
2113 .unwrap_or(false)
2114 {
2115 return true;
2116 }
2117 }
2118 false
2119}
2120
2121fn has_non_identity_content_encoding(headers: &str) -> bool {
2123 for line in headers.split("\r\n") {
2124 let Some((name, value)) = line.split_once(':') else {
2125 continue;
2126 };
2127 if !name.eq_ignore_ascii_case("content-encoding") {
2128 continue;
2129 }
2130 if value
2131 .split(',')
2132 .any(|encoding| !encoding.trim().eq_ignore_ascii_case("identity"))
2133 {
2134 return true;
2135 }
2136 }
2137 false
2138}
2139
2140fn replace_bytes(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Option<Vec<u8>> {
2145 if !contains_bytes(haystack, needle) {
2146 return None;
2147 }
2148
2149 let mut result = Vec::with_capacity(haystack.len());
2150 let mut cursor = 0;
2151 while cursor < haystack.len() {
2152 if haystack[cursor..].starts_with(needle) {
2153 result.extend_from_slice(replacement);
2154 cursor += needle.len();
2155 } else {
2156 result.push(haystack[cursor]);
2157 cursor += 1;
2158 }
2159 }
2160 Some(result)
2161}
2162
2163#[cfg(test)]
2165fn url_decoded_contains(haystack: &[u8], needle: &[u8]) -> bool {
2166 let decoded: Vec<u8> = percent_decode(haystack).collect();
2167 contains_bytes(&decoded, needle)
2168}
2169
2170#[cfg(test)]
2174fn json_escaped_contains(haystack: &[u8], needle: &[u8]) -> bool {
2175 let decoded = json_unescape(haystack);
2176 contains_bytes(&decoded, needle)
2177}
2178
2179fn json_unescape(haystack: &[u8]) -> Vec<u8> {
2181 let mut decoded = Vec::with_capacity(haystack.len());
2182 let mut i = 0;
2183 while i < haystack.len() {
2184 if haystack[i] == b'\\'
2185 && i + 5 < haystack.len()
2186 && haystack[i + 1] == b'u'
2187 && let (Some(a), Some(b), Some(c), Some(d)) = (
2188 hex_digit(haystack[i + 2]),
2189 hex_digit(haystack[i + 3]),
2190 hex_digit(haystack[i + 4]),
2191 hex_digit(haystack[i + 5]),
2192 )
2193 {
2194 let cp = ((a as u32) << 12) | ((b as u32) << 8) | ((c as u32) << 4) | (d as u32);
2195 if let Some(ch) = char::from_u32(cp) {
2196 let mut buf = [0u8; 4];
2197 decoded.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
2198 }
2199 i += 6;
2200 continue;
2201 }
2202 decoded.push(haystack[i]);
2203 i += 1;
2204 }
2205 decoded
2206}
2207
2208fn hex_digit(b: u8) -> Option<u8> {
2209 (b as char).to_digit(16).map(|d| d as u8)
2210}
2211
2212fn update_content_length(headers: &str, new_len: usize) -> String {
2217 let mut result = String::with_capacity(headers.len());
2218 for (i, line) in headers.split("\r\n").enumerate() {
2219 if i > 0 {
2220 result.push_str("\r\n");
2221 }
2222 if line
2223 .as_bytes()
2224 .get(..15)
2225 .is_some_and(|b| b.eq_ignore_ascii_case(b"content-length:"))
2226 {
2227 result.push_str(&format!("Content-Length: {new_len}"));
2228 } else {
2229 result.push_str(line);
2230 }
2231 }
2232 result
2233}
2234
2235fn find_header_boundary(data: &[u8]) -> Option<usize> {
2237 data.windows(4)
2238 .position(|w| w == b"\r\n\r\n")
2239 .map(|pos| pos + 4)
2240}
2241
2242fn append_chunk(output: &mut Vec<u8>, payload: &[u8]) {
2243 if payload.is_empty() {
2244 return;
2245 }
2246 output.extend_from_slice(format!("{:X}\r\n", payload.len()).as_bytes());
2247 output.extend_from_slice(payload);
2248 output.extend_from_slice(b"\r\n");
2249}
2250
2251fn detect_blocking_action_with_tail(
2252 ineligible_for_substitution: &[IneligibleSecret],
2253 prev_tail: &[u8],
2254 data: &[u8],
2255 headers: &str,
2256) -> Option<BlockingAction> {
2257 if ineligible_for_substitution.is_empty() {
2258 return None;
2259 }
2260
2261 let scan_buf: Cow<[u8]> = if prev_tail.is_empty() {
2262 Cow::Borrowed(data)
2263 } else {
2264 let mut stitched = Vec::with_capacity(prev_tail.len() + data.len());
2265 stitched.extend_from_slice(prev_tail);
2266 stitched.extend_from_slice(data);
2267 Cow::Owned(stitched)
2268 };
2269 let scan = scan_buf.as_ref();
2270 let url_decoded = scan
2271 .contains(&b'%')
2272 .then(|| percent_decode(scan).collect::<Vec<u8>>());
2273 let json_decoded = scan
2274 .windows(2)
2275 .any(|window| window == b"\\u")
2276 .then(|| json_unescape(scan));
2277 let basic_auth_credentials = decoded_basic_auth_credentials(headers);
2278
2279 let mut detected = None;
2280 for secret in ineligible_for_substitution {
2281 let needle = secret.placeholder.as_bytes();
2282 if contains_bytes(scan, needle)
2283 || url_decoded
2284 .as_deref()
2285 .is_some_and(|decoded| contains_bytes(decoded, needle))
2286 || json_decoded
2287 .as_deref()
2288 .is_some_and(|decoded| contains_bytes(decoded, needle))
2289 || basic_auth_credentials
2290 .iter()
2291 .any(|decoded| decoded.contains(&secret.placeholder))
2292 {
2293 detected = Some(strictest_violation_action(detected, secret.action));
2294 }
2295 }
2296
2297 detected
2298}
2299
2300fn update_tail_buffer(tail: &mut Vec<u8>, data: &[u8], tail_size: usize) {
2301 if tail_size == 0 {
2302 tail.clear();
2303 return;
2304 }
2305 if data.len() >= tail_size {
2306 tail.clear();
2307 tail.extend_from_slice(&data[data.len() - tail_size..]);
2308 return;
2309 }
2310 tail.extend_from_slice(data);
2311 let overflow = tail.len().saturating_sub(tail_size);
2312 if overflow > 0 {
2313 tail.drain(..overflow);
2314 }
2315}
2316
2317fn consume_chunked_body(
2320 state: &mut ChunkedBodyState,
2321 data: &[u8],
2322) -> Result<Option<usize>, ViolationAction> {
2323 process_chunked_body(state, data, |_| Ok(()))
2324}
2325
2326fn process_chunked_body<E>(
2330 state: &mut ChunkedBodyState,
2331 data: &[u8],
2332 mut on_event: E,
2333) -> Result<Option<usize>, ViolationAction>
2334where
2335 E: FnMut(ChunkedBodyEvent<'_>) -> Result<(), ViolationAction>,
2336{
2337 let mut cursor = 0;
2338 while cursor < data.len() {
2339 let phase = std::mem::replace(&mut state.phase, ChunkedPhase::SizeLine);
2340 match phase {
2341 ChunkedPhase::SizeLine => {
2342 state.line.push(data[cursor]);
2343 cursor += 1;
2344 if state.line.len() > MAX_HTTP_HEADER_BYTES {
2345 return Err(ViolationAction::Block);
2346 }
2347 if state.line.ends_with(b"\r\n") {
2348 let line = &state.line[..state.line.len() - 2];
2349 let size = parse_chunk_size(line)?;
2350 state.line.clear();
2351 state.phase = if size == 0 {
2352 on_event(ChunkedBodyEvent::ZeroChunk)?;
2353 ChunkedPhase::TrailerLine
2354 } else {
2355 ChunkedPhase::Data { remaining: size }
2356 };
2357 } else {
2358 state.phase = ChunkedPhase::SizeLine;
2359 }
2360 }
2361 ChunkedPhase::Data { mut remaining } => {
2362 let take = remaining.min(data.len() - cursor);
2363 on_event(ChunkedBodyEvent::Payload(&data[cursor..cursor + take]))?;
2364 cursor += take;
2365 remaining -= take;
2366 if remaining == 0 {
2367 state.phase = ChunkedPhase::DataCrlf { seen_cr: false };
2368 } else {
2369 state.phase = ChunkedPhase::Data { remaining };
2370 }
2371 }
2372 ChunkedPhase::DataCrlf { mut seen_cr } => {
2373 if !seen_cr {
2374 if data[cursor] != b'\r' {
2375 return Err(ViolationAction::Block);
2376 }
2377 seen_cr = true;
2378 cursor += 1;
2379 state.phase = ChunkedPhase::DataCrlf { seen_cr };
2380 } else {
2381 if data[cursor] != b'\n' {
2382 return Err(ViolationAction::Block);
2383 }
2384 state.phase = ChunkedPhase::SizeLine;
2385 cursor += 1;
2386 }
2387 }
2388 ChunkedPhase::TrailerLine => {
2389 state.line.push(data[cursor]);
2390 cursor += 1;
2391 if state.line.len() > MAX_HTTP_HEADER_BYTES {
2392 return Err(ViolationAction::Block);
2393 }
2394 if state.line.ends_with(b"\r\n") {
2395 let is_empty = state.line.len() == 2;
2396 on_event(ChunkedBodyEvent::TrailerLine(&state.line))?;
2397 state.line.clear();
2398 if is_empty {
2399 return Ok(Some(cursor));
2400 }
2401 state.phase = ChunkedPhase::TrailerLine;
2402 } else {
2403 state.phase = ChunkedPhase::TrailerLine;
2404 }
2405 }
2406 }
2407 }
2408
2409 Ok(None)
2410}
2411
2412fn parse_chunk_size(line: &[u8]) -> Result<usize, ViolationAction> {
2413 let size = line
2414 .split(|byte| *byte == b';')
2415 .next()
2416 .unwrap_or_default()
2417 .trim_ascii();
2418 if size.is_empty() {
2419 return Err(ViolationAction::Block);
2420 }
2421 let size = std::str::from_utf8(size).map_err(|_| ViolationAction::Block)?;
2422 usize::from_str_radix(size, 16).map_err(|_| ViolationAction::Block)
2423}
2424
2425fn strictest_violation_action(
2428 current: Option<BlockingAction>,
2429 candidate: BlockingAction,
2430) -> BlockingAction {
2431 match (current, candidate) {
2432 (Some(BlockingAction::BlockAndTerminate), _) | (_, BlockingAction::BlockAndTerminate) => {
2433 BlockingAction::BlockAndTerminate
2434 }
2435 (Some(BlockingAction::BlockAndLog), _) | (_, BlockingAction::BlockAndLog) => {
2436 BlockingAction::BlockAndLog
2437 }
2438 (Some(BlockingAction::Block), _) | (None, BlockingAction::Block) => BlockingAction::Block,
2439 }
2440}
2441
2442#[cfg(test)]
2447mod tests {
2448 use super::*;
2449 use crate::secrets::config::*;
2450 use crate::shared::{ResolvedHostnameFamily, SharedState};
2451
2452 use std::net::{IpAddr, Ipv4Addr};
2453 use std::time::Duration;
2454
2455 fn make_config(secrets: Vec<SecretEntry>) -> SecretsConfig {
2456 SecretsConfig {
2457 secrets,
2458 on_violation: ViolationAction::Block,
2459 }
2460 }
2461
2462 fn make_secret(placeholder: &str, value: &str, host: &str) -> SecretEntry {
2463 SecretEntry {
2464 env_var: "TEST_KEY".into(),
2465 value: value.into(),
2466 placeholder: placeholder.into(),
2467 allowed_hosts: vec![HostPattern::Exact(host.into())],
2468 injection: SecretInjection::default(),
2469 on_violation: None,
2470 require_tls_identity: true,
2471 }
2472 }
2473
2474 fn cache_host(shared: &SharedState, host: &str, ip: Ipv4Addr) {
2475 shared.cache_resolved_hostname(
2476 host,
2477 ResolvedHostnameFamily::Ipv4,
2478 [IpAddr::V4(ip)],
2479 Duration::from_secs(60),
2480 );
2481 }
2482
2483 fn basic_auth_only() -> SecretInjection {
2484 SecretInjection {
2485 headers: false,
2486 basic_auth: true,
2487 query_params: false,
2488 body: false,
2489 }
2490 }
2491
2492 fn split_http_body(data: &[u8]) -> (&[u8], &[u8]) {
2493 let boundary = find_header_boundary(data).expect("HTTP header boundary");
2494 data.split_at(boundary)
2495 }
2496
2497 fn decode_chunked_payload(data: &[u8]) -> (Vec<u8>, Vec<u8>, usize) {
2498 let mut cursor = 0;
2499 let mut decoded = Vec::new();
2500 let mut trailers = Vec::new();
2501
2502 loop {
2503 let line_end = data[cursor..]
2504 .windows(2)
2505 .position(|window| window == b"\r\n")
2506 .map(|pos| cursor + pos)
2507 .expect("chunk size line");
2508 let size = parse_chunk_size(&data[cursor..line_end]).expect("valid chunk size");
2509 cursor = line_end + 2;
2510
2511 if size == 0 {
2512 loop {
2513 let trailer_end = data[cursor..]
2514 .windows(2)
2515 .position(|window| window == b"\r\n")
2516 .map(|pos| cursor + pos + 2)
2517 .expect("trailer line");
2518 trailers.extend_from_slice(&data[cursor..trailer_end]);
2519 let empty = trailer_end - cursor == 2;
2520 cursor = trailer_end;
2521 if empty {
2522 return (decoded, trailers, cursor);
2523 }
2524 }
2525 }
2526
2527 decoded.extend_from_slice(&data[cursor..cursor + size]);
2528 cursor += size;
2529 assert_eq!(&data[cursor..cursor + 2], b"\r\n");
2530 cursor += 2;
2531 }
2532 }
2533
2534 fn encode_h2_header_block(headers: &[(&[u8], &[u8])]) -> Vec<u8> {
2535 let mut encoder = HpackEncoder::with_dynamic_size(4096);
2536 let mut block = Vec::new();
2537 for (name, value) in headers {
2538 encoder
2539 .encode(
2540 (name.to_vec(), value.to_vec(), HpackEncoder::NEVER_INDEXED),
2541 &mut block,
2542 )
2543 .unwrap();
2544 }
2545 block
2546 }
2547
2548 fn h2_request(headers: &[(&[u8], &[u8])], end_stream: bool) -> Vec<u8> {
2549 let encoded = encode_h2_header_block(headers);
2550 let mut out = HTTP2_PREFACE.to_vec();
2551 append_http2_frame(&mut out, 0x4, 0, 0, &[]).unwrap();
2552 append_http2_header_frames(&mut out, 1, end_stream, &encoded).unwrap();
2553 out
2554 }
2555
2556 fn h2_request_with_split_headers(headers: &[(&[u8], &[u8])], split_at: usize) -> Vec<u8> {
2557 let encoded = encode_h2_header_block(headers);
2558 let split_at = split_at.min(encoded.len());
2559 let mut out = HTTP2_PREFACE.to_vec();
2560 append_http2_frame(&mut out, 0x4, 0, 0, &[]).unwrap();
2561 append_http2_frame(&mut out, HTTP2_FRAME_HEADERS, 0, 1, &encoded[..split_at]).unwrap();
2562 append_http2_frame(
2563 &mut out,
2564 HTTP2_FRAME_CONTINUATION,
2565 HTTP2_FLAG_END_HEADERS | HTTP2_FLAG_END_STREAM,
2566 1,
2567 &encoded[split_at..],
2568 )
2569 .unwrap();
2570 out
2571 }
2572
2573 fn h2_request_with_data(headers: &[(&[u8], &[u8])], data: &[u8]) -> Vec<u8> {
2574 let mut out = h2_request(headers, false);
2575 append_http2_frame(&mut out, HTTP2_FRAME_DATA, HTTP2_FLAG_END_STREAM, 1, data).unwrap();
2576 out
2577 }
2578
2579 fn append_h2_headers(
2580 out: &mut Vec<u8>,
2581 stream_id: u32,
2582 headers: &[(&[u8], &[u8])],
2583 end_stream: bool,
2584 ) {
2585 let encoded = encode_h2_header_block(headers);
2586 append_http2_header_frames(out, stream_id, end_stream, &encoded).unwrap();
2587 }
2588
2589 fn decode_first_h2_headers(data: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
2590 assert!(data.starts_with(HTTP2_PREFACE));
2591 let mut cursor = HTTP2_PREFACE.len();
2592 let mut decoder = HpackDecoder::with_dynamic_size(4096);
2593 let mut header_block = Vec::new();
2594 let mut in_headers = false;
2595
2596 while cursor + 9 <= data.len() {
2597 let len = http2_frame_payload_len(&data[cursor..cursor + 9]);
2598 let raw = &data[cursor..cursor + 9 + len];
2599 cursor += 9 + len;
2600 let frame = parse_http2_frame(raw).unwrap();
2601 match frame.kind {
2602 HTTP2_FRAME_HEADERS => {
2603 header_block.extend_from_slice(
2604 http2_headers_fragment(frame.flags, frame.payload).unwrap(),
2605 );
2606 if frame.flags & HTTP2_FLAG_END_HEADERS != 0 {
2607 break;
2608 }
2609 in_headers = true;
2610 }
2611 HTTP2_FRAME_CONTINUATION if in_headers => {
2612 header_block.extend_from_slice(frame.payload);
2613 if frame.flags & HTTP2_FLAG_END_HEADERS != 0 {
2614 break;
2615 }
2616 }
2617 _ => {}
2618 }
2619 }
2620
2621 let mut encoded = header_block;
2622 let mut headers = Vec::new();
2623 decoder.decode(&mut encoded, &mut headers).unwrap();
2624 headers
2625 .into_iter()
2626 .map(|(name, value, _flags)| (name, value))
2627 .collect()
2628 }
2629
2630 fn h2_header_value(headers: &[(Vec<u8>, Vec<u8>)], name: &[u8]) -> String {
2631 let value = headers
2632 .iter()
2633 .find(|(header_name, _)| header_name.eq_ignore_ascii_case(name))
2634 .map(|(_, value)| value.as_slice())
2635 .expect("header present");
2636 String::from_utf8(value.to_vec()).unwrap()
2637 }
2638
2639 #[test]
2640 fn substitute_in_headers() {
2641 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2642 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2643
2644 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2645 let output = handler.substitute(input).unwrap();
2646 assert_eq!(
2647 String::from_utf8(output.into_owned()).unwrap(),
2648 "GET / HTTP/1.1\r\nAuthorization: Bearer real-secret\r\n\r\n"
2649 );
2650 }
2651
2652 #[test]
2653 fn no_substitute_for_wrong_host() {
2654 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2655 let mut handler = SecretsHandler::new(&config, "evil.com", true);
2656
2657 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2658 assert_eq!(
2659 handler.substitute(input).unwrap_err(),
2660 ViolationAction::Block
2661 );
2662 }
2663
2664 #[test]
2665 fn split_http1_post_is_not_misclassified_as_http2_preface() {
2666 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2667 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2668
2669 assert_eq!(handler.substitute(b"P").unwrap().as_ref(), b"");
2670
2671 let output = handler
2672 .substitute(b"OST / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n")
2673 .unwrap();
2674 assert_eq!(
2675 String::from_utf8(output.into_owned()).unwrap(),
2676 "POST / HTTP/1.1\r\nAuthorization: Bearer real-secret\r\n\r\n"
2677 );
2678 }
2679
2680 #[test]
2681 fn allowed_placeholder_substitutes_when_another_secret_is_ineligible() {
2682 let allowed = make_secret("$ALLOWED", "allowed-secret", "api.openai.com");
2683 let blocked = make_secret("$BLOCKED", "blocked-secret", "api.github.com");
2684 let config = make_config(vec![allowed, blocked]);
2685 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2686
2687 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $ALLOWED\r\n\r\n";
2688 let output = handler.substitute(input).unwrap();
2689
2690 assert_eq!(
2691 String::from_utf8(output.into_owned()).unwrap(),
2692 "GET / HTTP/1.1\r\nAuthorization: Bearer allowed-secret\r\n\r\n"
2693 );
2694 }
2695
2696 #[test]
2697 fn global_passthrough_host_forwards_placeholder_unchanged() {
2698 let mut config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2699 config.on_violation =
2700 ViolationAction::Passthrough(vec![HostPattern::Exact("api.anthropic.com".into())]);
2701 let mut handler = SecretsHandler::new(&config, "api.anthropic.com", true);
2702
2703 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2704 let output = handler.substitute(input).unwrap();
2705 assert_eq!(&*output, input);
2706 }
2707
2708 #[test]
2709 fn per_secret_passthrough_host_forwards_placeholder_unchanged() {
2710 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2711 secret.on_violation = Some(ViolationAction::Passthrough(vec![HostPattern::Exact(
2712 "api.anthropic.com".into(),
2713 )]));
2714 let config = make_config(vec![secret]);
2715 let mut handler = SecretsHandler::new(&config, "api.anthropic.com", true);
2716
2717 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2718 let output = handler.substitute(input).unwrap();
2719 assert_eq!(&*output, input);
2720 }
2721
2722 #[test]
2723 fn global_passthrough_action_forwards_disallowed_placeholder_unchanged() {
2724 let mut config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2725 config.on_violation = ViolationAction::Passthrough(vec![HostPattern::Any]);
2726 let mut handler = SecretsHandler::new(&config, "evil.com", true);
2727
2728 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2729 let output = handler.substitute(input).unwrap();
2730 assert_eq!(&*output, input);
2731 }
2732
2733 #[test]
2734 fn passthrough_only_connection_has_no_handler_work() {
2735 let mut config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2736 config.on_violation = ViolationAction::Passthrough(vec![HostPattern::Any]);
2737 let handler = SecretsHandler::new(&config, "evil.com", true);
2738
2739 assert!(handler.is_empty());
2740 }
2741
2742 #[test]
2743 fn passthrough_host_does_not_allow_other_disallowed_placeholders() {
2744 let mut passthrough = make_secret("$PASSTHROUGH", "real-secret-a", "api.openai.com");
2745 passthrough.on_violation = Some(ViolationAction::Passthrough(vec![HostPattern::Exact(
2746 "api.anthropic.com".into(),
2747 )]));
2748 let blocked = make_secret("$BLOCKED", "real-secret-b", "api.github.com");
2749 let config = make_config(vec![passthrough, blocked]);
2750 let mut handler = SecretsHandler::new(&config, "api.anthropic.com", true);
2751
2752 let input = b"GET / HTTP/1.1\r\nX-A: $PASSTHROUGH\r\nX-B: $BLOCKED\r\n\r\n";
2753 assert_eq!(
2754 handler.substitute(input).unwrap_err(),
2755 ViolationAction::Block
2756 );
2757 }
2758
2759 #[test]
2760 fn per_secret_passthrough_blocks_for_non_matching_host() {
2761 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2762 secret.on_violation = Some(ViolationAction::Passthrough(vec![HostPattern::Exact(
2763 "api.anthropic.com".into(),
2764 )]));
2765 let config = make_config(vec![secret]);
2766 let mut handler = SecretsHandler::new(&config, "evil.com", true);
2767
2768 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2769 assert_eq!(
2770 handler.substitute(input).unwrap_err(),
2771 ViolationAction::Block
2772 );
2773 }
2774
2775 #[test]
2776 fn global_passthrough_blocks_for_non_matching_host() {
2777 let mut config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2778 config.on_violation =
2779 ViolationAction::Passthrough(vec![HostPattern::Exact("api.anthropic.com".into())]);
2780 let mut handler = SecretsHandler::new(&config, "evil.com", true);
2781
2782 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2783 assert_eq!(
2784 handler.substitute(input).unwrap_err(),
2785 ViolationAction::BlockAndLog
2786 );
2787 }
2788
2789 #[test]
2790 fn global_block_and_terminate_marks_violation_as_terminating() {
2791 let mut config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2792 config.on_violation = ViolationAction::BlockAndTerminate;
2793 let mut handler = SecretsHandler::new(&config, "evil.com", true);
2794
2795 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2796 assert_eq!(
2797 handler.substitute(input).unwrap_err(),
2798 ViolationAction::BlockAndTerminate
2799 );
2800 }
2801
2802 #[test]
2803 fn per_secret_block_and_terminate_marks_violation_as_terminating() {
2804 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2805 secret.on_violation = Some(ViolationAction::BlockAndTerminate);
2806 let config = make_config(vec![secret]);
2807 let mut handler = SecretsHandler::new(&config, "evil.com", true);
2808
2809 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
2810 assert_eq!(
2811 handler.substitute(input).unwrap_err(),
2812 ViolationAction::BlockAndTerminate
2813 );
2814 }
2815
2816 #[test]
2817 fn body_injection_disabled_by_default() {
2818 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2819 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2820
2821 let input = b"POST / HTTP/1.1\r\nContent-Length: 15\r\n\r\n{\"key\": \"$KEY\"}";
2822 let output = handler.substitute(input).unwrap();
2823 assert!(
2824 String::from_utf8(output.into_owned())
2825 .unwrap()
2826 .contains("$KEY")
2827 );
2828 }
2829
2830 #[test]
2831 fn body_injection_when_enabled() {
2832 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2833 secret.injection.body = true;
2834 let config = make_config(vec![secret]);
2835 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2836
2837 let input = b"POST / HTTP/1.1\r\nContent-Length: 15\r\n\r\n{\"key\": \"$KEY\"}";
2838 let output = handler.substitute(input).unwrap();
2839 assert_eq!(
2840 String::from_utf8(output.into_owned()).unwrap(),
2841 "POST / HTTP/1.1\r\nContent-Length: 22\r\n\r\n{\"key\": \"real-secret\"}"
2842 );
2843 }
2844
2845 #[test]
2846 fn body_injection_updates_content_length() {
2847 let mut secret = make_secret("$KEY", "a]longer]secret]value", "api.openai.com");
2848 secret.injection.body = true;
2849 let config = make_config(vec![secret]);
2850 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2851
2852 let body = "{\"key\": \"$KEY\"}";
2853 let input = format!(
2854 "POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n{}",
2855 body.len(),
2856 body
2857 );
2858 let output = handler.substitute(input.as_bytes()).unwrap();
2859 let result = String::from_utf8(output.into_owned()).unwrap();
2860
2861 let expected_body = "{\"key\": \"a]longer]secret]value\"}";
2862 assert!(result.contains(expected_body));
2863 assert!(result.contains(&format!("Content-Length: {}", expected_body.len())));
2864 }
2865
2866 #[test]
2867 fn body_injection_buffers_until_content_length_complete() {
2868 let mut secret = make_secret("$KEY", "longer-secret", "api.openai.com");
2869 secret.injection.body = true;
2870 let config = make_config(vec![secret]);
2871 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2872
2873 let body = b"{\"key\":\"$KEY\"}";
2874 let mut chunk1 = format!(
2875 "POST / HTTP/1.1\r\nHost: api.openai.com\r\nContent-Length: {}\r\n\r\n",
2876 body.len()
2877 )
2878 .into_bytes();
2879 chunk1.extend_from_slice(&body[..5]);
2880
2881 let out1 = handler.substitute(&chunk1).unwrap();
2882 assert!(out1.is_empty());
2883
2884 let out2 = handler.substitute(&body[5..]).unwrap();
2885 let result = String::from_utf8(out2.into_owned()).unwrap();
2886 let expected_body = "{\"key\":\"longer-secret\"}";
2887 assert!(result.contains(expected_body));
2888 assert!(result.contains(&format!("Content-Length: {}", expected_body.len())));
2889 }
2890
2891 #[test]
2892 fn body_injection_blocks_content_length_over_buffer_limit() {
2893 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2894 secret.injection.body = true;
2895 let config = make_config(vec![secret]);
2896 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2897
2898 let input = format!(
2899 "POST / HTTP/1.1\r\nHost: api.openai.com\r\nContent-Length: {}\r\n\r\n",
2900 MAX_HTTP_BODY_BUFFER_BYTES + 1
2901 );
2902
2903 assert_eq!(
2904 handler.substitute(input.as_bytes()).unwrap_err(),
2905 ViolationAction::Block
2906 );
2907 }
2908
2909 #[test]
2910 fn invalid_content_length_is_blocked() {
2911 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2912 secret.injection.body = true;
2913 let config = make_config(vec![secret]);
2914 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2915
2916 let input =
2917 b"POST / HTTP/1.1\r\nHost: api.openai.com\r\nContent-Length: nope\r\n\r\nxx$KEYyy";
2918
2919 assert_eq!(
2920 handler.substitute(input).unwrap_err(),
2921 ViolationAction::Block
2922 );
2923 }
2924
2925 #[test]
2926 fn conflicting_content_lengths_are_blocked() {
2927 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
2928 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2929
2930 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";
2931
2932 assert_eq!(
2933 handler.substitute(input).unwrap_err(),
2934 ViolationAction::Block
2935 );
2936 }
2937
2938 #[test]
2939 fn body_injection_no_content_length_header() {
2940 let mut secret = make_secret("$KEY", "longer-secret", "api.openai.com");
2941 secret.injection.body = true;
2942 let config = make_config(vec![secret]);
2943 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2944
2945 let input =
2948 b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nF\r\n{\"key\": \"$KEY\"}\r\n0\r\n\r\n";
2949 let output = handler.substitute(input).unwrap();
2950 let result = String::from_utf8(output.into_owned()).unwrap();
2951 assert!(!result.contains("$KEY"));
2952 assert!(result.contains("longer-secret"));
2953 assert!(!result.contains("Content-Length"));
2954 }
2955
2956 #[test]
2957 fn chunked_body_injection_rewrites_split_placeholder_across_chunks() {
2958 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2959 secret.injection.body = true;
2960 let config = make_config(vec![secret]);
2961 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2962
2963 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";
2964 let output = handler.substitute(input).unwrap().into_owned();
2965 let (_, body) = split_http_body(&output);
2966 let (decoded, trailers, consumed) = decode_chunked_payload(body);
2967
2968 assert_eq!(decoded, b"xxreal-secret");
2969 assert_eq!(trailers, b"\r\n");
2970 assert_eq!(consumed, body.len());
2971 }
2972
2973 #[test]
2974 fn chunked_body_injection_rewrites_placeholder_split_across_tls_reads() {
2975 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2976 secret.injection.body = true;
2977 let config = make_config(vec![secret]);
2978 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2979
2980 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";
2981 let chunk2 = b"2\r\nEY\r\n0\r\n\r\n";
2982
2983 let mut output = handler.substitute(chunk1).unwrap().into_owned();
2984 output.extend_from_slice(handler.substitute(chunk2).unwrap().as_ref());
2985 let (_, body) = split_http_body(&output);
2986 let (decoded, trailers, consumed) = decode_chunked_payload(body);
2987
2988 assert_eq!(decoded, b"xxreal-secret");
2989 assert_eq!(trailers, b"\r\n");
2990 assert_eq!(consumed, body.len());
2991 }
2992
2993 #[test]
2994 fn chunked_body_injection_preserves_trailers_and_recurses_to_next_request() {
2995 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
2996 secret.injection.body = true;
2997 let config = make_config(vec![secret]);
2998 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
2999
3000 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();
3001 input.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: api.openai.com\r\nAuth: $KEY\r\n\r\n");
3002
3003 let output = handler.substitute(&input).unwrap().into_owned();
3004 let (_, body_and_next) = split_http_body(&output);
3005 let (decoded, trailers, consumed) = decode_chunked_payload(body_and_next);
3006 let next_request = &body_and_next[consumed..];
3007
3008 assert_eq!(decoded, b"real-secret");
3009 assert_eq!(trailers, b"X-Trailer: yes\r\n\r\n");
3010 assert_eq!(
3011 next_request,
3012 b"GET /b HTTP/1.1\r\nHost: api.openai.com\r\nAuth: real-secret\r\n\r\n"
3013 );
3014 }
3015
3016 #[test]
3017 fn chunked_body_injection_skips_content_encoded_body() {
3018 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
3019 secret.injection.body = true;
3020 let config = make_config(vec![secret]);
3021 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3022
3023 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";
3024 let output = handler.substitute(input).unwrap();
3025
3026 assert_eq!(output.as_ref(), input);
3027 }
3028
3029 #[test]
3030 fn split_chunked_body_payload_blocks_for_wrong_host() {
3031 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3032 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3033
3034 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";
3035
3036 assert_eq!(
3037 handler.substitute(input).unwrap_err(),
3038 ViolationAction::Block
3039 );
3040 }
3041
3042 #[test]
3043 fn oversized_secret_placeholder_is_rejected() {
3044 let placeholder = "x".repeat(MAX_SECRET_PLACEHOLDER_BYTES + 1);
3045 let config = make_config(vec![make_secret(
3046 &placeholder,
3047 "real-secret",
3048 "api.openai.com",
3049 )]);
3050 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3051
3052 assert_eq!(
3053 handler.substitute(b"GET / HTTP/1.1\r\n\r\n").unwrap_err(),
3054 ViolationAction::Block
3055 );
3056 }
3057
3058 #[test]
3059 fn header_only_substitution_preserves_content_length() {
3060 let config = make_config(vec![make_secret("$KEY", "longer-value", "api.openai.com")]);
3061 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3062
3063 let input =
3064 b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\nContent-Length: 5\r\n\r\nhello";
3065 let output = handler.substitute(input).unwrap();
3066 let result = String::from_utf8(output.into_owned()).unwrap();
3067 assert!(result.contains("Content-Length: 5"));
3069 assert!(result.ends_with("hello"));
3070 }
3071
3072 #[test]
3073 fn eligible_secret_preserves_binary_body_without_placeholder() {
3074 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3075 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3076
3077 let body = vec![0x1f, 0x8b, 0x08, 0x00, 0xff, 0x00, 0x80, 0xfe];
3078 let mut input = format!(
3079 "POST /git-upload-pack HTTP/1.1\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\n\r\n",
3080 body.len()
3081 )
3082 .into_bytes();
3083 input.extend_from_slice(&body);
3084
3085 let output = handler.substitute(&input).unwrap();
3086 assert_eq!(&*output, input.as_slice());
3087 }
3088
3089 #[test]
3090 fn body_injection_skips_content_encoded_body() {
3091 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
3092 secret.injection.body = true;
3093 let config = make_config(vec![secret]);
3094 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3095
3096 let body = b"compressed-looking-$KEY-bytes";
3097 let mut input = format!(
3098 "POST /git-upload-pack HTTP/1.1\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\n\r\n",
3099 body.len()
3100 )
3101 .into_bytes();
3102 input.extend_from_slice(body);
3103
3104 let output = handler.substitute(&input).unwrap();
3105 assert_eq!(&*output, input.as_slice());
3106 }
3107
3108 #[test]
3109 fn eligible_secret_preserves_binary_chunk_without_placeholder() {
3110 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3111 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3112
3113 let input = [0x1f, 0x8b, 0x08, 0x00, 0xff, 0x00, 0x80, 0xfe];
3114 let output = handler.substitute(&input).unwrap();
3115 assert_eq!(&*output, input.as_slice());
3116 }
3117
3118 #[test]
3119 fn body_injection_preserves_non_utf8_bytes() {
3120 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
3121 secret.injection.body = true;
3122 let config = make_config(vec![secret]);
3123 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3124
3125 let body = [0xff, b'$', b'K', b'E', b'Y', 0xfe];
3126 let mut input =
3127 format!("POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n", body.len()).into_bytes();
3128 input.extend_from_slice(&body);
3129
3130 let output = handler.substitute(&input).unwrap().into_owned();
3131 let expected_body = [b"\xffreal-secret".as_slice(), &[0xfe]].concat();
3132 let expected = [
3133 format!(
3134 "POST / HTTP/1.1\r\nContent-Length: {}\r\n\r\n",
3135 expected_body.len()
3136 )
3137 .as_bytes(),
3138 expected_body.as_slice(),
3139 ]
3140 .concat();
3141
3142 assert_eq!(output, expected);
3143 }
3144
3145 #[test]
3146 fn no_secrets_passthrough() {
3147 let config = make_config(vec![]);
3148 let mut handler = SecretsHandler::new(&config, "anything.com", true);
3149
3150 let input = b"GET / HTTP/1.1\r\n\r\n";
3151 let output = handler.substitute(input).unwrap();
3152 assert_eq!(&*output, input);
3153 }
3154
3155 #[test]
3156 fn require_tls_identity_blocks_on_non_intercepted() {
3157 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3158 let mut handler = SecretsHandler::new(&config, "api.openai.com", false);
3160
3161 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\n\r\n";
3162 let output = handler.substitute(input).unwrap();
3163 assert!(
3165 String::from_utf8(output.into_owned())
3166 .unwrap()
3167 .contains("$KEY")
3168 );
3169 }
3170
3171 #[test]
3172 fn basic_auth_only_does_not_substitute_other_schemes() {
3173 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
3174 secret.injection = basic_auth_only();
3175 let config = make_config(vec![secret]);
3176 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3177
3178 let input = b"GET / HTTP/1.1\r\nAuthorization: Bearer $KEY\r\nX-Custom: $KEY\r\n\r\n";
3180 let output = handler.substitute(input).unwrap();
3181 let result = String::from_utf8(output.into_owned()).unwrap();
3182 assert!(result.contains("Authorization: Bearer $KEY"));
3183 assert!(result.contains("X-Custom: $KEY"));
3184 }
3185
3186 #[test]
3187 fn basic_auth_decodes_substitutes_and_reencodes_credentials() {
3188 let mut user = make_secret("$MSB_USER", "alice", "api.openai.com");
3189 user.env_var = "USER".into();
3190 user.injection = basic_auth_only();
3191 let mut password = make_secret("$MSB_PASSWORD", "s3cr3t", "api.openai.com");
3192 password.env_var = "PASSWORD".into();
3193 password.injection = basic_auth_only();
3194 let config = make_config(vec![user, password]);
3195 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3196
3197 let encoded = BASE64.encode(b"$MSB_USER:$MSB_PASSWORD");
3198 let input = format!("GET / HTTP/1.1\r\nAuthorization: Basic {encoded}\r\n\r\n");
3199 let output = handler.substitute(input.as_bytes()).unwrap();
3200 let result = String::from_utf8(output.into_owned()).unwrap();
3201
3202 assert!(result.contains(&format!(
3203 "Authorization: Basic {}",
3204 BASE64.encode(b"alice:s3cr3t")
3205 )));
3206 assert!(!result.contains("$MSB_USER"));
3207 assert!(!result.contains("$MSB_PASSWORD"));
3208 }
3209
3210 #[test]
3211 fn basic_auth_encoded_placeholder_is_blocked_for_wrong_host() {
3212 let mut secret = make_secret("$MSB_PASSWORD", "s3cr3t", "api.openai.com");
3213 secret.injection = basic_auth_only();
3214 let config = make_config(vec![secret]);
3215 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3216
3217 let encoded = BASE64.encode(b"user:$MSB_PASSWORD");
3218 let input = format!("GET / HTTP/1.1\r\nAuthorization: Basic {encoded}\r\n\r\n");
3219
3220 assert_eq!(
3221 handler.substitute(input.as_bytes()).unwrap_err(),
3222 ViolationAction::Block
3223 );
3224 }
3225
3226 #[test]
3227 fn basic_auth_encoded_placeholder_is_not_replaced_when_scope_disabled() {
3228 let mut secret = make_secret("$MSB_PASSWORD", "s3cr3t", "api.openai.com");
3229 secret.injection = SecretInjection {
3230 headers: false,
3231 basic_auth: false,
3232 query_params: false,
3233 body: false,
3234 };
3235 let config = make_config(vec![secret]);
3236 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3237
3238 let encoded = BASE64.encode(b"user:$MSB_PASSWORD");
3239 let input = format!("GET / HTTP/1.1\r\nAuthorization: Basic {encoded}\r\n\r\n");
3240 let output = handler.substitute(input.as_bytes()).unwrap();
3241
3242 assert_eq!(String::from_utf8(output.into_owned()).unwrap(), input);
3243 }
3244
3245 #[test]
3246 fn query_params_substitution() {
3247 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
3248 secret.injection = SecretInjection {
3249 headers: false,
3250 basic_auth: false,
3251 query_params: true,
3252 body: false,
3253 };
3254 let config = make_config(vec![secret]);
3255 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3256
3257 let input = b"GET /api?key=$KEY HTTP/1.1\r\nHost: api.openai.com\r\n\r\n";
3258 let output = handler.substitute(input).unwrap();
3259 let result = String::from_utf8(output.into_owned()).unwrap();
3260 assert!(result.contains("GET /api?key=real-secret HTTP/1.1"));
3262 }
3264
3265 #[test]
3266 fn query_params_do_not_substitute_path() {
3267 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
3268 secret.injection = SecretInjection {
3269 headers: false,
3270 basic_auth: false,
3271 query_params: true,
3272 body: false,
3273 };
3274 let config = make_config(vec![secret]);
3275 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3276
3277 let input = b"GET /path/$KEY?token=$KEY HTTP/1.1\r\nHost: api.openai.com\r\n\r\n";
3278 let output = handler.substitute(input).unwrap();
3279 let result = String::from_utf8(output.into_owned()).unwrap();
3280
3281 assert!(result.contains("GET /path/$KEY?token=real-secret HTTP/1.1"));
3282 }
3283
3284 #[test]
3285 fn header_injection_does_not_substitute_request_line_query() {
3286 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3287 let mut handler = SecretsHandler::new(&config, "api.openai.com", true);
3288
3289 let input = b"GET /api?key=$KEY HTTP/1.1\r\nHost: api.openai.com\r\n\r\n";
3290 let output = handler.substitute(input).unwrap();
3291
3292 assert_eq!(output.as_ref(), input);
3293 }
3294
3295 #[test]
3296 fn url_encoded_placeholder_in_query_blocks_for_wrong_host() {
3297 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3298 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3299
3300 let input = b"GET /api?token=%24KEY HTTP/1.1\r\nHost: evil.com\r\n\r\n";
3302 assert_eq!(
3303 handler.substitute(input).unwrap_err(),
3304 ViolationAction::Block
3305 );
3306 }
3307
3308 #[test]
3309 fn url_encoded_placeholder_in_body_blocks_for_wrong_host() {
3310 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3311 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3312
3313 let input = b"POST / HTTP/1.1\r\nContent-Length: 13\r\n\r\nkey=%24KEY&x=1";
3314 assert_eq!(
3315 handler.substitute(input).unwrap_err(),
3316 ViolationAction::Block
3317 );
3318 }
3319
3320 #[test]
3321 fn json_escaped_placeholder_in_body_blocks_for_wrong_host() {
3322 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3323 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3324
3325 let input =
3327 b"POST / HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{\"k\":\"\\u0024KEY\"}";
3328 assert_eq!(
3329 handler.substitute(input).unwrap_err(),
3330 ViolationAction::Block
3331 );
3332 }
3333
3334 #[test]
3335 fn split_url_encoded_placeholder_blocks_for_wrong_host() {
3336 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3337 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3338
3339 let chunk1 = b"POST / HTTP/1.1\r\nHost: evil.com\r\nContent-Length: 14\r\n\r\nkey=%24K";
3340 let chunk2 = b"EY&x=1";
3341
3342 assert!(handler.substitute(chunk1).is_ok());
3343 assert_eq!(
3344 handler.substitute(chunk2).unwrap_err(),
3345 ViolationAction::Block
3346 );
3347 }
3348
3349 #[test]
3350 fn split_json_escaped_placeholder_blocks_for_wrong_host() {
3351 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3352 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3353
3354 let chunk1 =
3355 b"POST / HTTP/1.1\r\nHost: evil.com\r\nContent-Length: 17\r\n\r\n{\"k\":\"\\u0024K";
3356 let chunk2 = b"EY\"}";
3357
3358 assert!(handler.substitute(chunk1).is_ok());
3359 assert_eq!(
3360 handler.substitute(chunk2).unwrap_err(),
3361 ViolationAction::Block
3362 );
3363 }
3364
3365 #[test]
3366 fn placeholder_split_across_writes_blocks_for_wrong_host() {
3367 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3368 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3369
3370 let first = b"GET / HTTP/1.1\r\nX-Token: $K";
3372 let second = b"EY\r\nHost: evil.com\r\n\r\n";
3373
3374 assert!(handler.substitute(first).is_ok());
3376 assert_eq!(
3378 handler.substitute(second).unwrap_err(),
3379 ViolationAction::Block
3380 );
3381 }
3382
3383 #[test]
3384 fn split_headers_do_not_leak_header_secret_into_body() {
3385 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3386 let mut handler = SecretsHandler::new(&config, "example.com", true);
3387
3388 let chunk1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 8\r\n";
3389 let out1 = handler.substitute(chunk1).unwrap();
3390 assert!(out1.is_empty());
3391
3392 let chunk2 = b"\r\nxx$KEYyy";
3393 let out2 = handler.substitute(chunk2).unwrap();
3394 let result = String::from_utf8(out2.into_owned()).unwrap();
3395
3396 assert!(result.contains("xx$KEYyy"));
3397 assert!(!result.contains("real-secret"));
3398 }
3399
3400 #[test]
3401 fn url_decoded_contains_basic() {
3402 assert!(url_decoded_contains(b"foo%24KEYbar", b"$KEY"));
3403 assert!(!url_decoded_contains(b"fooKEYbar", b"$KEY"));
3404 assert!(url_decoded_contains(b"%2", b"%2"));
3406 }
3407
3408 #[test]
3409 fn json_escaped_contains_basic() {
3410 assert!(json_escaped_contains(b"\"\\u0024KEY\"", b"$KEY"));
3411 assert!(json_escaped_contains(
3412 b"\\u0024\\u004B\\u0045\\u0059",
3413 b"$KEY"
3414 ));
3415 assert!(!json_escaped_contains(b"KEY", b"$KEY"));
3416 }
3417
3418 #[test]
3419 fn body_in_separate_chunk_preserves_non_utf8_bytes() {
3420 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3432 let mut handler = SecretsHandler::new(&config, "example.com", true);
3433
3434 let chunk1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 13\r\n\r\n";
3436 handler.substitute(chunk1).unwrap();
3437
3438 let mut body: Vec<u8> = vec![0x00, 0x80, 0xc0, 0xff, 0xfe];
3441 body.extend_from_slice(b"$KEY");
3442 body.extend_from_slice(&[0x81, 0xc1, 0xee, 0xef]);
3443 assert_eq!(body.len(), 13);
3444
3445 let out = handler.substitute(&body).unwrap();
3446 assert_eq!(out.as_ref(), body.as_slice());
3447 }
3448
3449 #[test]
3450 fn body_split_across_two_chunks_round_trips() {
3451 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3460 let mut handler = SecretsHandler::new(&config, "example.com", true);
3461
3462 let mut body: Vec<u8> = vec![0x00, 0x80, 0xc0, 0xff, 0xfe, 0xfd, 0xfc];
3463 body.extend_from_slice(b"$KEY");
3464 body.extend_from_slice(&[0x81, 0xc1, 0xee, 0xef]);
3465 assert_eq!(body.len(), 15);
3466
3467 let mut chunk1 =
3468 b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 15\r\n\r\n".to_vec();
3469 chunk1.extend_from_slice(&body[..5]);
3470
3471 let out1 = handler.substitute(&chunk1).unwrap();
3472 let boundary = out1
3473 .windows(4)
3474 .position(|w| w == b"\r\n\r\n")
3475 .map(|p| p + 4)
3476 .unwrap();
3477 assert_eq!(&out1[boundary..], &body[..5]);
3478
3479 let out2 = handler.substitute(&body[5..]).unwrap();
3480 assert_eq!(out2.as_ref(), &body[5..]);
3481 }
3482
3483 #[test]
3484 fn framing_state_resets_after_request_completes() {
3485 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3489 let mut handler = SecretsHandler::new(&config, "example.com", true);
3490
3491 let body: Vec<u8> = vec![0x00, 0x80, 0xc0, 0xff, 0xfe];
3492 let mut chunk1 =
3493 b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n".to_vec();
3494 chunk1.extend_from_slice(&body);
3495 handler.substitute(&chunk1).unwrap();
3496
3497 let chunk2 = b"GET /b HTTP/1.1\r\nHost: example.com\r\n\r\n";
3500 let out2 = handler.substitute(chunk2).unwrap();
3501 assert_eq!(out2.as_ref(), chunk2.as_slice());
3502 }
3503
3504 #[test]
3505 fn violation_detected_in_body_continuation_chunk() {
3506 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3510 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3511
3512 let chunk1 = b"POST /a HTTP/1.1\r\nHost: evil.com\r\nContent-Length: 16\r\n\r\n";
3513 handler.substitute(chunk1).unwrap();
3514
3515 let chunk2 = b"prefix:$KEY:suffix";
3516 assert_eq!(
3517 handler.substitute(chunk2).unwrap_err(),
3518 ViolationAction::Block
3519 );
3520 }
3521
3522 #[test]
3523 fn header_only_secret_does_not_leak_into_body_continuation_chunk() {
3524 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3532 let mut handler = SecretsHandler::new(&config, "example.com", true);
3533
3534 let chunk1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 24\r\n\r\n";
3536 handler.substitute(chunk1).unwrap();
3537
3538 let body = b"prefix:$KEY:more-padding";
3542 assert_eq!(body.len(), 24);
3543 let out = handler.substitute(body).unwrap();
3544 assert_eq!(out.as_ref(), body.as_slice());
3545 }
3546
3547 #[test]
3548 fn pipelined_request_in_body_continuation_chunk_is_substituted() {
3549 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3555 let mut handler = SecretsHandler::new(&config, "example.com", true);
3556
3557 let mut chunk1 =
3559 b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n".to_vec();
3560 chunk1.extend_from_slice(b"abcd");
3561 handler.substitute(&chunk1).unwrap();
3562
3563 let mut chunk2 = b"e".to_vec();
3566 chunk2.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
3567
3568 let out = handler.substitute(&chunk2).unwrap();
3569
3570 let mut expected = b"e".to_vec();
3571 expected.extend_from_slice(
3572 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
3573 );
3574 assert_eq!(out.as_ref(), expected.as_slice());
3575 }
3576
3577 #[test]
3578 fn pipelined_request_in_same_chunk_as_headers_is_substituted() {
3579 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3585 let mut handler = SecretsHandler::new(&config, "example.com", true);
3586
3587 let mut chunk =
3588 b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n".to_vec();
3589 chunk.extend_from_slice(b"abcde");
3590 chunk.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
3591
3592 let out = handler.substitute(&chunk).unwrap();
3593
3594 let mut expected =
3595 b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n".to_vec();
3596 expected.extend_from_slice(b"abcde");
3597 expected.extend_from_slice(
3598 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
3599 );
3600 assert_eq!(out.as_ref(), expected.as_slice());
3601 }
3602
3603 #[test]
3604 fn three_pipelined_requests_in_one_chunk_all_substitute() {
3605 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3609 let mut handler = SecretsHandler::new(&config, "example.com", true);
3610
3611 let r1 =
3612 b"POST /a HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\nContent-Length: 3\r\n\r\nbod";
3613 let r2 =
3614 b"PUT /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\nContent-Length: 2\r\n\r\nXY";
3615 let r3 = b"GET /c HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n";
3616 let mut chunk = Vec::new();
3617 chunk.extend_from_slice(r1);
3618 chunk.extend_from_slice(r2);
3619 chunk.extend_from_slice(r3);
3620
3621 let out = handler.substitute(&chunk).unwrap();
3622
3623 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";
3624 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";
3625 let r3_out = b"GET /c HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n";
3626 let mut expected = Vec::new();
3627 expected.extend_from_slice(r1_out);
3628 expected.extend_from_slice(r2_out);
3629 expected.extend_from_slice(r3_out);
3630
3631 assert_eq!(out.as_ref(), expected.as_slice());
3632 }
3633
3634 #[test]
3635 fn pipelined_spillover_without_substitution_stays_zero_copy() {
3636 let config = make_config(vec![make_secret("$KEY", "real-secret", "other.com")]);
3640 let mut handler = SecretsHandler::new(&config, "example.com", true);
3641
3642 let r1 = b"POST /a HTTP/1.1\r\nHost: example.com\r\nContent-Length: 3\r\n\r\nbod";
3643 let r2 = b"GET /b HTTP/1.1\r\nHost: example.com\r\n\r\n";
3644 let mut chunk = Vec::new();
3645 chunk.extend_from_slice(r1);
3646 chunk.extend_from_slice(r2);
3647
3648 let out = handler.substitute(&chunk).unwrap();
3649 assert!(matches!(out, Cow::Borrowed(_)));
3650 assert_eq!(out.as_ref(), chunk.as_slice());
3651 }
3652
3653 #[test]
3654 fn violation_in_pipelined_next_request_basic_auth_is_detected() {
3655 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3663 let mut handler = SecretsHandler::new(&config, "evil.com", true);
3664
3665 let chunk1 = b"POST /a HTTP/1.1\r\nHost: evil.com\r\nContent-Length: 3\r\n\r\n";
3666 handler.substitute(chunk1).unwrap();
3667
3668 let mut chunk2 = b"foo".to_vec();
3671 chunk2.extend_from_slice(
3672 b"POST /b HTTP/1.1\r\nHost: evil.com\r\nAuthorization: Basic YWRtaW46JEtFWQ==\r\n\r\n",
3673 );
3674 assert_eq!(
3675 handler.substitute(&chunk2).unwrap_err(),
3676 ViolationAction::Block
3677 );
3678 }
3679
3680 #[test]
3681 fn pipelined_get_without_content_length_recurses_into_next_request() {
3682 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3688 let mut handler = SecretsHandler::new(&config, "example.com", true);
3689
3690 let mut chunk = b"GET /a HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
3691 chunk.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
3692
3693 let out = handler.substitute(&chunk).unwrap();
3694
3695 let mut expected = b"GET /a HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
3696 expected.extend_from_slice(
3697 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
3698 );
3699 assert_eq!(out.as_ref(), expected.as_slice());
3700 }
3701
3702 #[test]
3703 fn substitution_resumes_after_chunked_request_body_terminator() {
3704 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
3710 let mut handler = SecretsHandler::new(&config, "example.com", true);
3711
3712 let chunk1 = b"POST /a HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
3714 handler.substitute(chunk1).unwrap();
3715
3716 let mut chunk2 = b"5\r\nhello\r\n0\r\n\r\n".to_vec();
3719 chunk2.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
3720
3721 let out = handler.substitute(&chunk2).unwrap();
3722
3723 let mut expected = b"5\r\nhello\r\n0\r\n\r\n".to_vec();
3724 expected.extend_from_slice(
3725 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
3726 );
3727 assert_eq!(out.as_ref(), expected.as_slice());
3728 }
3729
3730 #[test]
3731 fn exact_host_requires_dns_pin_for_tls_intercepted_secret() {
3732 let ip = Ipv4Addr::new(203, 0, 113, 10);
3733 let shared = SharedState::new(16);
3734 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3735 let mut handler =
3736 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3737
3738 let input = b"GET / HTTP/1.1\r\nHost: api.openai.com\r\nAuthorization: Bearer $KEY\r\n\r\n";
3739 assert_eq!(
3740 handler.substitute(input).unwrap_err(),
3741 ViolationAction::Block
3742 );
3743
3744 cache_host(&shared, "api.openai.com", ip);
3745 let mut handler =
3746 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3747 let output = handler.substitute(input).unwrap();
3748
3749 assert!(
3750 String::from_utf8(output.into_owned())
3751 .unwrap()
3752 .contains("real-secret")
3753 );
3754 }
3755
3756 #[test]
3757 fn any_host_bypasses_dns_pin_for_tls_intercepted_secret() {
3758 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
3759 secret.allowed_hosts = vec![HostPattern::Any];
3760 let config = make_config(vec![secret]);
3761 let shared = SharedState::new(16);
3762 let mut handler = SecretsHandler::new_tls_intercepted(
3763 &config,
3764 "unresolved.example",
3765 IpAddr::V4(Ipv4Addr::new(203, 0, 113, 20)),
3766 &shared,
3767 );
3768
3769 let input =
3770 b"GET / HTTP/1.1\r\nHost: unresolved.example\r\nAuthorization: Bearer $KEY\r\n\r\n";
3771 let output = handler.substitute(input).unwrap();
3772
3773 assert!(
3774 String::from_utf8(output.into_owned())
3775 .unwrap()
3776 .contains("real-secret")
3777 );
3778 }
3779
3780 #[test]
3781 fn host_alias_matches_gateway_without_dns_pin() {
3782 let gateway = Ipv4Addr::new(192, 0, 2, 1);
3783 let shared = SharedState::new(16);
3784 shared.set_gateway_ips(Some(gateway), None);
3785
3786 let config = make_config(vec![make_secret("$KEY", "real-secret", crate::HOST_ALIAS)]);
3787 let mut handler = SecretsHandler::new_tls_intercepted(
3788 &config,
3789 crate::HOST_ALIAS,
3790 IpAddr::V4(gateway),
3791 &shared,
3792 );
3793
3794 let input = format!(
3795 "GET / HTTP/1.1\r\nHost: {}\r\nAuthorization: Bearer $KEY\r\n\r\n",
3796 crate::HOST_ALIAS
3797 );
3798 let output = handler.substitute(input.as_bytes()).unwrap();
3799
3800 assert!(
3801 String::from_utf8(output.into_owned())
3802 .unwrap()
3803 .contains("real-secret")
3804 );
3805 }
3806
3807 #[test]
3808 fn tls_intercepted_http_host_must_match_sni() {
3809 let ip = Ipv4Addr::new(203, 0, 113, 30);
3810 let shared = SharedState::new(16);
3811 cache_host(&shared, "api.openai.com", ip);
3812 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3813 let mut handler =
3814 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3815
3816 let input = b"GET / HTTP/1.1\r\nHost: evil.com\r\nAuthorization: Bearer $KEY\r\n\r\n";
3817 assert_eq!(
3818 handler.substitute(input).unwrap_err(),
3819 ViolationAction::Block
3820 );
3821 }
3822
3823 #[test]
3824 fn tls_intercepted_http_host_validation_buffers_split_headers() {
3825 let ip = Ipv4Addr::new(203, 0, 113, 31);
3826 let shared = SharedState::new(16);
3827 cache_host(&shared, "api.openai.com", ip);
3828 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3829 let mut handler =
3830 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3831
3832 let out1 = handler
3833 .substitute(b"GET / HTTP/1.1\r\nHost: evil.com\r\n")
3834 .unwrap();
3835 assert!(out1.is_empty());
3836 assert_eq!(
3837 handler
3838 .substitute(b"Authorization: Bearer $KEY\r\n\r\n")
3839 .unwrap_err(),
3840 ViolationAction::Block
3841 );
3842 }
3843
3844 #[test]
3845 fn tls_intercepted_http_host_validation_survives_leading_empty_block() {
3846 let ip = Ipv4Addr::new(203, 0, 113, 32);
3847 let shared = SharedState::new(16);
3848 cache_host(&shared, "api.openai.com", ip);
3849 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3850 let mut handler =
3851 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3852
3853 assert_eq!(
3854 handler.substitute(b"\r\n\r\n").unwrap().as_ref(),
3855 b"\r\n\r\n"
3856 );
3857 assert_eq!(
3858 handler
3859 .substitute(b"GET / HTTP/1.1\r\nHost: evil.com\r\nAuth: $KEY\r\n\r\n")
3860 .unwrap_err(),
3861 ViolationAction::Block
3862 );
3863 }
3864
3865 #[test]
3866 fn tls_intercepted_http2_authority_must_match_sni() {
3867 let ip = Ipv4Addr::new(203, 0, 113, 33);
3868 let shared = SharedState::new(16);
3869 cache_host(&shared, "api.openai.com", ip);
3870 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3871 let mut handler =
3872 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3873
3874 let request = h2_request(
3875 &[
3876 (b":method", b"GET"),
3877 (b":scheme", b"https"),
3878 (b":authority", b"evil.com"),
3879 (b":path", b"/"),
3880 (b"authorization", b"Bearer $KEY"),
3881 ],
3882 true,
3883 );
3884
3885 assert_eq!(
3886 handler.substitute(&request).unwrap_err(),
3887 ViolationAction::Block
3888 );
3889 }
3890
3891 #[test]
3892 fn tls_intercepted_http2_substitutes_header_secret() {
3893 let ip = Ipv4Addr::new(203, 0, 113, 34);
3894 let shared = SharedState::new(16);
3895 cache_host(&shared, "api.openai.com", ip);
3896 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3897 let mut handler =
3898 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3899
3900 let request = h2_request(
3901 &[
3902 (b":method", b"GET"),
3903 (b":scheme", b"https"),
3904 (b":authority", b"api.openai.com"),
3905 (b":path", b"/"),
3906 (b"authorization", b"Bearer $KEY"),
3907 ],
3908 true,
3909 );
3910
3911 let output = handler.substitute(&request).unwrap().into_owned();
3912 let headers = decode_first_h2_headers(&output);
3913 assert_eq!(
3914 h2_header_value(&headers, b"authorization"),
3915 "Bearer real-secret"
3916 );
3917 }
3918
3919 #[test]
3920 fn tls_intercepted_http2_preface_can_span_tls_reads() {
3921 let ip = Ipv4Addr::new(203, 0, 113, 38);
3922 let shared = SharedState::new(16);
3923 cache_host(&shared, "api.openai.com", ip);
3924 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3925 let mut handler =
3926 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3927
3928 let request = h2_request(
3929 &[
3930 (b":method", b"GET"),
3931 (b":scheme", b"https"),
3932 (b":authority", b"api.openai.com"),
3933 (b":path", b"/"),
3934 (b"authorization", b"Bearer $KEY"),
3935 ],
3936 true,
3937 );
3938
3939 assert_eq!(handler.substitute(&request[..1]).unwrap().as_ref(), b"");
3940
3941 let output = handler.substitute(&request[1..]).unwrap().into_owned();
3942 let headers = decode_first_h2_headers(&output);
3943 assert_eq!(
3944 h2_header_value(&headers, b"authorization"),
3945 "Bearer real-secret"
3946 );
3947 }
3948
3949 #[test]
3950 fn tls_intercepted_http2_substitutes_query_and_basic_auth() {
3951 let ip = Ipv4Addr::new(203, 0, 113, 35);
3952 let shared = SharedState::new(16);
3953 cache_host(&shared, "api.openai.com", ip);
3954 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
3955 secret.injection = SecretInjection {
3956 headers: false,
3957 basic_auth: true,
3958 query_params: true,
3959 body: false,
3960 };
3961 let config = make_config(vec![secret]);
3962 let mut handler =
3963 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3964 let auth = format!("Basic {}", BASE64.encode(b"user:$KEY"));
3965
3966 let request = h2_request(
3967 &[
3968 (b":method", b"GET"),
3969 (b":scheme", b"https"),
3970 (b":authority", b"api.openai.com"),
3971 (b":path", b"/v1/$KEY?token=$KEY"),
3972 (b"authorization", auth.as_bytes()),
3973 ],
3974 true,
3975 );
3976
3977 let output = handler.substitute(&request).unwrap().into_owned();
3978 let headers = decode_first_h2_headers(&output);
3979 assert_eq!(
3980 h2_header_value(&headers, b":path"),
3981 "/v1/$KEY?token=real-secret"
3982 );
3983 let auth = h2_header_value(&headers, b"authorization");
3984 let decoded = split_auth_scheme(&auth)
3985 .and_then(|(_, encoded)| BASE64.decode(encoded).ok())
3986 .and_then(|bytes| String::from_utf8(bytes).ok())
3987 .unwrap();
3988 assert_eq!(decoded, "user:real-secret");
3989 }
3990
3991 #[test]
3992 fn tls_intercepted_http2_split_header_block_is_validated() {
3993 let ip = Ipv4Addr::new(203, 0, 113, 36);
3994 let shared = SharedState::new(16);
3995 cache_host(&shared, "api.openai.com", ip);
3996 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
3997 let mut handler =
3998 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
3999
4000 let request = h2_request_with_split_headers(
4001 &[
4002 (b":method", b"GET"),
4003 (b":scheme", b"https"),
4004 (b":authority", b"evil.com"),
4005 (b":path", b"/"),
4006 (b"authorization", b"Bearer $KEY"),
4007 ],
4008 8,
4009 );
4010
4011 assert_eq!(
4012 handler.substitute(&request).unwrap_err(),
4013 ViolationAction::Block
4014 );
4015 }
4016
4017 #[test]
4018 fn tls_intercepted_http2_body_placeholder_blocks_until_body_rewrite_exists() {
4019 let ip = Ipv4Addr::new(203, 0, 113, 37);
4020 let shared = SharedState::new(16);
4021 cache_host(&shared, "api.openai.com", ip);
4022 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4023 secret.injection.body = true;
4024 let config = make_config(vec![secret]);
4025 let mut handler =
4026 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4027
4028 let request = h2_request_with_data(
4029 &[
4030 (b":method", b"POST"),
4031 (b":scheme", b"https"),
4032 (b":authority", b"api.openai.com"),
4033 (b":path", b"/"),
4034 ],
4035 b"{\"key\":\"$KEY\"}",
4036 );
4037
4038 assert_eq!(
4039 handler.substitute(&request).unwrap_err(),
4040 ViolationAction::Block
4041 );
4042 }
4043
4044 #[test]
4045 fn tls_intercepted_http2_body_placeholder_split_across_data_frames_blocks() {
4046 let ip = Ipv4Addr::new(203, 0, 113, 39);
4047 let shared = SharedState::new(16);
4048 cache_host(&shared, "api.openai.com", ip);
4049 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4050 secret.injection.body = true;
4051 let config = make_config(vec![secret]);
4052 let mut handler =
4053 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4054
4055 let mut request = HTTP2_PREFACE.to_vec();
4056 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
4057 append_h2_headers(
4058 &mut request,
4059 1,
4060 &[
4061 (b":method", b"POST"),
4062 (b":scheme", b"https"),
4063 (b":authority", b"api.openai.com"),
4064 (b":path", b"/"),
4065 ],
4066 false,
4067 );
4068 append_http2_frame(&mut request, HTTP2_FRAME_DATA, 0, 1, b"$KE").unwrap();
4069 append_http2_frame(
4070 &mut request,
4071 HTTP2_FRAME_DATA,
4072 HTTP2_FLAG_END_STREAM,
4073 1,
4074 b"Y",
4075 )
4076 .unwrap();
4077
4078 assert_eq!(
4079 handler.substitute(&request).unwrap_err(),
4080 ViolationAction::Block
4081 );
4082 }
4083
4084 #[test]
4085 fn tls_intercepted_http2_data_tails_are_tracked_per_stream() {
4086 let ip = Ipv4Addr::new(203, 0, 113, 40);
4087 let shared = SharedState::new(16);
4088 cache_host(&shared, "api.openai.com", ip);
4089 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4090 secret.injection.body = true;
4091 let config = make_config(vec![secret]);
4092 let mut handler =
4093 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4094
4095 let mut request = HTTP2_PREFACE.to_vec();
4096 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
4097 for stream_id in [1, 3] {
4098 append_h2_headers(
4099 &mut request,
4100 stream_id,
4101 &[
4102 (b":method", b"POST"),
4103 (b":scheme", b"https"),
4104 (b":authority", b"api.openai.com"),
4105 (b":path", b"/"),
4106 ],
4107 false,
4108 );
4109 }
4110 append_http2_frame(&mut request, HTTP2_FRAME_DATA, 0, 1, b"$KE").unwrap();
4111 append_http2_frame(
4112 &mut request,
4113 HTTP2_FRAME_DATA,
4114 HTTP2_FLAG_END_STREAM,
4115 3,
4116 b"Y",
4117 )
4118 .unwrap();
4119
4120 assert!(handler.substitute(&request).is_ok());
4121 }
4122
4123 #[test]
4124 fn tls_intercepted_http2_large_data_frame_without_placeholder_passes() {
4125 let ip = Ipv4Addr::new(203, 0, 113, 41);
4126 let shared = SharedState::new(16);
4127 cache_host(&shared, "api.openai.com", ip);
4128 let mut secret = make_secret("$KEY", "real-secret", "api.openai.com");
4129 secret.injection.body = true;
4130 let config = make_config(vec![secret]);
4131 let mut handler =
4132 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4133 let payload = vec![b'a'; 1024 * 1024];
4134
4135 let request = h2_request_with_data(
4136 &[
4137 (b":method", b"POST"),
4138 (b":scheme", b"https"),
4139 (b":authority", b"api.openai.com"),
4140 (b":path", b"/"),
4141 ],
4142 &payload,
4143 );
4144
4145 let output = handler.substitute(&request).unwrap().into_owned();
4146 assert!(output.ends_with(&payload));
4147 }
4148
4149 #[test]
4150 fn tls_intercepted_http2_data_before_headers_is_blocked() {
4151 let ip = Ipv4Addr::new(203, 0, 113, 42);
4152 let shared = SharedState::new(16);
4153 cache_host(&shared, "api.openai.com", ip);
4154 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4155 let mut handler =
4156 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4157
4158 let mut request = HTTP2_PREFACE.to_vec();
4159 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
4160 append_http2_frame(
4161 &mut request,
4162 HTTP2_FRAME_DATA,
4163 HTTP2_FLAG_END_STREAM,
4164 1,
4165 b"body",
4166 )
4167 .unwrap();
4168
4169 assert_eq!(
4170 handler.substitute(&request).unwrap_err(),
4171 ViolationAction::Block
4172 );
4173 }
4174
4175 #[test]
4176 fn tls_intercepted_http2_decoded_header_list_size_is_bounded() {
4177 let ip = Ipv4Addr::new(203, 0, 113, 43);
4178 let shared = SharedState::new(16);
4179 cache_host(&shared, "api.openai.com", ip);
4180 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4181 let mut handler =
4182 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4183 let mut encoder = HpackEncoder::with_dynamic_size(4096);
4184
4185 let mut first_block = Vec::new();
4186 for (name, value) in [
4187 (b":method".as_slice(), b"GET".as_slice()),
4188 (b":scheme".as_slice(), b"https".as_slice()),
4189 (b":authority".as_slice(), b"api.openai.com".as_slice()),
4190 (b":path".as_slice(), b"/".as_slice()),
4191 ] {
4192 encoder
4193 .encode(
4194 (name.to_vec(), value.to_vec(), HpackEncoder::NEVER_INDEXED),
4195 &mut first_block,
4196 )
4197 .unwrap();
4198 }
4199 encoder
4200 .encode(
4201 (
4202 b"x-fill".to_vec(),
4203 vec![b'a'; 4000],
4204 HpackEncoder::WITH_INDEXING,
4205 ),
4206 &mut first_block,
4207 )
4208 .unwrap();
4209
4210 let mut second_block = Vec::new();
4211 for (name, value) in [
4212 (b":method".as_slice(), b"GET".as_slice()),
4213 (b":scheme".as_slice(), b"https".as_slice()),
4214 (b":authority".as_slice(), b"api.openai.com".as_slice()),
4215 (b":path".as_slice(), b"/".as_slice()),
4216 ] {
4217 encoder
4218 .encode(
4219 (name.to_vec(), value.to_vec(), HpackEncoder::NEVER_INDEXED),
4220 &mut second_block,
4221 )
4222 .unwrap();
4223 }
4224 for _ in 0..20 {
4225 encoder.encode(62u32, &mut second_block).unwrap();
4226 }
4227
4228 let mut request = HTTP2_PREFACE.to_vec();
4229 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
4230 append_http2_header_frames(&mut request, 1, true, &first_block).unwrap();
4231 append_http2_header_frames(&mut request, 3, true, &second_block).unwrap();
4232
4233 assert_eq!(
4234 handler.substitute(&request).unwrap_err(),
4235 ViolationAction::Block
4236 );
4237 }
4238
4239 #[test]
4240 fn tls_intercepted_http2_limits_concurrent_open_streams() {
4241 let ip = Ipv4Addr::new(203, 0, 113, 44);
4242 let shared = SharedState::new(16);
4243 cache_host(&shared, "api.openai.com", ip);
4244 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4245 let mut handler =
4246 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4247
4248 let mut request = HTTP2_PREFACE.to_vec();
4249 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
4250 for i in 0..=MAX_HTTP2_TRACKED_STREAMS {
4251 append_h2_headers(
4252 &mut request,
4253 1 + (i as u32 * 2),
4254 &[
4255 (b":method", b"POST"),
4256 (b":scheme", b"https"),
4257 (b":authority", b"api.openai.com"),
4258 (b":path", b"/"),
4259 ],
4260 false,
4261 );
4262 }
4263
4264 assert_eq!(
4265 handler.substitute(&request).unwrap_err(),
4266 ViolationAction::Block
4267 );
4268 }
4269
4270 #[test]
4271 fn tls_intercepted_http2_closed_streams_release_tracking_state() {
4272 let ip = Ipv4Addr::new(203, 0, 113, 45);
4273 let shared = SharedState::new(16);
4274 cache_host(&shared, "api.openai.com", ip);
4275 let config = make_config(vec![make_secret("$KEY", "real-secret", "api.openai.com")]);
4276 let mut handler =
4277 SecretsHandler::new_tls_intercepted(&config, "api.openai.com", IpAddr::V4(ip), &shared);
4278
4279 let mut request = HTTP2_PREFACE.to_vec();
4280 append_http2_frame(&mut request, 0x4, 0, 0, &[]).unwrap();
4281 for i in 0..=MAX_HTTP2_TRACKED_STREAMS {
4282 append_h2_headers(
4283 &mut request,
4284 1 + (i as u32 * 2),
4285 &[
4286 (b":method", b"GET"),
4287 (b":scheme", b"https"),
4288 (b":authority", b"api.openai.com"),
4289 (b":path", b"/"),
4290 ],
4291 true,
4292 );
4293 }
4294
4295 assert!(handler.substitute(&request).is_ok());
4296 }
4297
4298 #[test]
4299 fn chunked_body_internal_terminator_bytes_do_not_end_request() {
4300 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
4301 let mut handler = SecretsHandler::new(&config, "example.com", true);
4302
4303 let chunk1 = b"POST /a HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
4304 handler.substitute(chunk1).unwrap();
4305
4306 let mut chunk2 = b"B\r\nAA\r\n0\r\n\r\nBB\r\n0\r\n\r\n".to_vec();
4307 chunk2.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
4308
4309 let out = handler.substitute(&chunk2).unwrap();
4310
4311 let mut expected = b"B\r\nAA\r\n0\r\n\r\nBB\r\n0\r\n\r\n".to_vec();
4312 expected.extend_from_slice(
4313 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
4314 );
4315 assert_eq!(out.as_ref(), expected.as_slice());
4316 }
4317
4318 #[test]
4319 fn split_chunked_terminator_resumes_next_request() {
4320 let config = make_config(vec![make_secret("$KEY", "real-secret", "example.com")]);
4321 let mut handler = SecretsHandler::new(&config, "example.com", true);
4322
4323 let chunk1 = b"POST /a HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
4324 handler.substitute(chunk1).unwrap();
4325
4326 let chunk2 = b"5\r\nhello\r\n0\r";
4327 let out2 = handler.substitute(chunk2).unwrap();
4328 assert_eq!(out2.as_ref(), chunk2.as_slice());
4329
4330 let mut chunk3 = b"\n\r\n".to_vec();
4331 chunk3.extend_from_slice(b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: $KEY\r\n\r\n");
4332
4333 let out3 = handler.substitute(&chunk3).unwrap();
4334
4335 let mut expected = b"\n\r\n".to_vec();
4336 expected.extend_from_slice(
4337 b"GET /b HTTP/1.1\r\nHost: example.com\r\nAuth: real-secret\r\n\r\n",
4338 );
4339 assert_eq!(out3.as_ref(), expected.as_slice());
4340 }
4341}