Skip to content

Commit ee2de81

Browse files
authored
fix(sandbox): preserve encoded slash policy from proto (#1073)
1 parent 0914f3f commit ee2de81

4 files changed

Lines changed: 67 additions & 2 deletions

File tree

architecture/sandbox.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1017,7 +1017,7 @@ flowchart LR
10171017
| `L7Protocol` | `Rest`, `Sql` | Supported application protocols |
10181018
| `TlsMode` | `Auto` (default), `Skip` | TLS handling strategy — `Auto` peeks first bytes and terminates if TLS is detected; `Skip` bypasses detection entirely |
10191019
| `EnforcementMode` | `Audit`, `Enforce` | What to do on L7 deny (log-only vs block) |
1020-
| `L7EndpointConfig` | `{ protocol, tls, enforcement }` | Per-endpoint L7 configuration |
1020+
| `L7EndpointConfig` | `{ protocol, tls, enforcement, allow_encoded_slash }` | Per-endpoint L7 configuration |
10211021
| `L7Decision` | `{ allowed, reason, matched_rule }` | Result of L7 evaluation |
10221022
| `L7RequestInfo` | `{ action, target, query_params }` | HTTP method, path, and decoded query multimap for policy evaluation |
10231023

architecture/security-policy.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ Each endpoint defines a network destination and, optionally, L7 inspection behav
475475
| `access` | `string` | `""` | Shorthand preset for common L7 rule sets. Mutually exclusive with `rules`. |
476476
| `rules` | `L7Rule[]` | `[]` | Explicit L7 allow rules. Mutually exclusive with `access`. |
477477
| `allowed_ips` | `string[]` | `[]` | IP allowlist for SSRF override. Entries overlapping always-blocked ranges (loopback, link-local, unspecified) are rejected at load time. See [Private IP Access via `allowed_ips`](#private-ip-access-via-allowed_ips). |
478+
| `allow_encoded_slash` | `bool` | `false` | Preserves `%2F` inside L7 request path segments instead of rejecting the request. Required for endpoints such as npm scoped packages. |
478479

479480
#### `NetworkBinary`
480481

@@ -1462,7 +1463,7 @@ Evaluated on every CONNECT request and every forward proxy request. The same OPA
14621463
| `network_action` | Same input | `"allow"` if endpoint + binary matched, `"deny"` otherwise |
14631464
| `deny_reason` | Same input | Human-readable string explaining why access was denied |
14641465
| `matched_network_policy` | Same input | Name of the matched policy (for audit logging) |
1465-
| `matched_endpoint_config` | Same input | Raw endpoint object for L7 config extraction (returned if endpoint has `protocol` or `allowed_ips` field) |
1466+
| `matched_endpoint_config` | Same input | Raw endpoint object for L7 config extraction (returned if endpoint has `protocol`, `allowed_ips`, or explicit TLS config) |
14661467

14671468
### L7 Rules (per-request within tunnel)
14681469

crates/openshell-sandbox/src/opa.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,9 @@ fn proto_to_opa_data_json(proto: &ProtoSandboxPolicy, entrypoint_pid: u32) -> St
911911
.collect();
912912
ep["deny_rules"] = deny_rules.into();
913913
}
914+
if e.allow_encoded_slash {
915+
ep["allow_encoded_slash"] = true.into();
916+
}
914917
ep
915918
})
916919
.collect();
@@ -1946,6 +1949,63 @@ process:
19461949
assert_eq!(l7.enforcement, crate::l7::EnforcementMode::Enforce);
19471950
}
19481951

1952+
#[test]
1953+
fn l7_endpoint_config_preserves_proto_allow_encoded_slash() {
1954+
let mut network_policies = std::collections::HashMap::new();
1955+
network_policies.insert(
1956+
"npm".to_string(),
1957+
NetworkPolicyRule {
1958+
name: "npm".to_string(),
1959+
endpoints: vec![NetworkEndpoint {
1960+
host: "registry.npmjs.org".to_string(),
1961+
port: 443,
1962+
protocol: "rest".to_string(),
1963+
enforcement: "enforce".to_string(),
1964+
access: "read-only".to_string(),
1965+
allow_encoded_slash: true,
1966+
..Default::default()
1967+
}],
1968+
binaries: vec![NetworkBinary {
1969+
path: "/usr/bin/node".to_string(),
1970+
..Default::default()
1971+
}],
1972+
},
1973+
);
1974+
let proto = ProtoSandboxPolicy {
1975+
version: 1,
1976+
filesystem: Some(ProtoFs {
1977+
include_workdir: true,
1978+
read_only: vec![],
1979+
read_write: vec![],
1980+
}),
1981+
landlock: Some(openshell_core::proto::LandlockPolicy {
1982+
compatibility: "best_effort".to_string(),
1983+
}),
1984+
process: Some(ProtoProc {
1985+
run_as_user: "sandbox".to_string(),
1986+
run_as_group: "sandbox".to_string(),
1987+
}),
1988+
network_policies,
1989+
};
1990+
1991+
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
1992+
let input = NetworkInput {
1993+
host: "registry.npmjs.org".into(),
1994+
port: 443,
1995+
binary_path: PathBuf::from("/usr/bin/node"),
1996+
binary_sha256: "unused".into(),
1997+
ancestors: vec![],
1998+
cmdline_paths: vec![],
1999+
};
2000+
2001+
let config = engine
2002+
.query_endpoint_config(&input)
2003+
.unwrap()
2004+
.expect("endpoint config");
2005+
let l7 = crate::l7::parse_l7_config(&config).unwrap();
2006+
assert!(l7.allow_encoded_slash);
2007+
}
2008+
19492009
#[test]
19502010
fn l7_endpoint_config_none_for_l4_only() {
19512011
let engine = l7_engine();

docs/reference/policy-schema.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ Each endpoint defines a reachable destination and optional inspection rules.
161161
| `rules` | list of rule objects | No | Fine-grained per-method, per-path allow rules. Mutually exclusive with `access`. |
162162
| `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. |
163163
| `allowed_ips` | list of string | No | CIDR or IP allowlist for SSRF override. Entries overlapping loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), or unspecified (`0.0.0.0`) are rejected at load time. |
164+
| `allow_encoded_slash` | bool | No | When `true`, L7 request parsing preserves `%2F` inside path segments instead of rejecting it. Use this for registries and APIs such as npm scoped packages (`/@scope%2Fname`). Defaults to `false`. |
164165

165166
#### Access Levels
166167

@@ -262,6 +263,9 @@ network_policies:
262263
endpoints:
263264
- host: registry.npmjs.org
264265
port: 443
266+
protocol: rest
267+
access: read-only
268+
allow_encoded_slash: true
265269
binaries:
266270
- path: /usr/bin/npm
267271
- path: /usr/bin/node

0 commit comments

Comments
 (0)