diff --git a/architecture/security-policy.md b/architecture/security-policy.md index efbcee76..ccf9d411 100644 --- a/architecture/security-policy.md +++ b/architecture/security-policy.md @@ -564,7 +564,7 @@ If any condition fails, the proxy returns `403 Forbidden`. 7. Rewrites the request: absolute-form → origin-form (`GET /path HTTP/1.1`), strips hop-by-hop headers, adds `Via: 1.1 openshell-sandbox` and `Connection: close` 8. Forwards the rewritten request, then relays bidirectionally using `tokio::io::copy_bidirectional` (supports chunked transfer, SSE streams, and other long-lived responses with no idle timeout) -**V1 simplifications**: Forward proxy v1 injects `Connection: close` (no keep-alive) and does not perform L7 inspection on the forwarded traffic. Every forward proxy connection handles exactly one request-response exchange. +**V1 simplifications**: Forward proxy v1 injects `Connection: close` (no keep-alive) and does not perform L7 inspection on the forwarded traffic. To avoid bypassing endpoint L7 rules, forward proxy requests are rejected when the matched endpoint has L7 configuration; use CONNECT for those endpoints. Every forward proxy connection handles exactly one request-response exchange. **Implementation**: See `crates/openshell-sandbox/src/proxy.rs` -- `handle_forward_proxy()`, `parse_proxy_uri()`, `rewrite_forward_request()`. diff --git a/crates/openshell-sandbox/src/proxy.rs b/crates/openshell-sandbox/src/proxy.rs index 6fe26b37..dfb9e7da 100644 --- a/crates/openshell-sandbox/src/proxy.rs +++ b/crates/openshell-sandbox/src/proxy.rs @@ -1647,6 +1647,43 @@ async fn handle_forward_proxy( }; let policy_str = matched_policy.as_deref().unwrap_or("-"); + // Forward proxy requests are parsed and rewritten before relay, so they + // cannot safely enter the CONNECT-style per-request L7 inspection loop. + // Deny here when endpoint-level L7 rules are configured to avoid bypassing + // method/path policy enforcement. + if query_l7_config(&opa_engine, &decision, &host_lc, port).is_some() { + let reason = "forward proxy is not allowed for endpoints with L7 policy; use CONNECT"; + info!( + src_addr = %peer_addr.ip(), + src_port = peer_addr.port(), + proxy_addr = %local_addr, + dst_host = %host_lc, + dst_port = port, + method = %method, + path = %path, + binary = %binary_str, + binary_pid = %pid_str, + ancestors = %ancestors_str, + cmdline = %cmdline_str, + action = "deny", + engine = "opa", + policy = %policy_str, + reason = %reason, + "FORWARD", + ); + emit_denial_simple( + denial_tx, + &host_lc, + port, + &binary_str, + &decision, + reason, + "forward_l7", + ); + respond(client, b"HTTP/1.1 403 Forbidden\r\n\r\n").await?; + return Ok(()); + } + // 5. DNS resolution + SSRF defence (mirrors the CONNECT path logic). // - If allowed_ips is set: validate resolved IPs against the allowlist // (this is the SSRF override for private IP destinations).