Skip to content

No policy validation to prevent corrupted or malicious policies #33

Closed
1 of 1 issue completed
Closed

Description

@pimlock

Sandbox policies are loaded from the server via GetSandboxPolicy or from local files. The policy drives filesystem allowlists (Landlock), network allowlists (OPA), and process identity (run_as_user, run_as_group). There is no validation that the policy content is safe or well-formed beyond basic proto/type conversion. Corrupted or maliciously crafted policy could therefore cause security issues such as

  • Privilege escalation
  • Overly permissive network policy
  • Undefined behaviour

Source code:

  • The server returns policy as-is in crates/navigator-server/src/grpc.rs in get_sandbox_policy (lines 513-540): it fetches the sandbox, extracts spec.policy, and returns it. There is no validation of the policy fields (e.g. no check that run_as_user is not root, no validation of network_policies or filesystem paths).
  • In crates/navigator-sandbox/src/policy.rs, SandboxPolicy::try_from(proto) (lines 95-121) and the From impls for FilesystemPolicy, ProcessPolicy, etc. (lines 123-158) convert proto to Rust types but do not validate safety (e.g. no rejection of root, no path normalization or constraints).
  • File-based loading in crates/navigator-sandbox/src/lib.rs (lines 283-311) builds policy from OPA engine config or from gRPC; the YAML/rego file content is not validated for malicious or unsafe policy content before use.
  • Filesystem paths from proto are used directly in policy.rs (e.g. lines 125-127): proto.read_only.into_iter().map(PathBuf::from).collect() with no check for path traversal or sensitive directories.

Originally by @igurel on 2026-02-20T06:06:48.189-08:00

Activity

  1. johntmyers commented on Mar 5, 2026

    @johntmyers
    Collaborator

    🔒 security-review-agent

    Security Review

    Determination: Legitimate concern

    Summary

    The sandbox policy pipeline lacks semantic validation at creation time: filesystem paths are not normalized or constrained, run_as_user accepts arbitrary values (including root), and there are no bounds checks on policy fields. While existing mitigations (Landlock, container namespaces, immutable-after-creation enforcement) limit exploitability, this represents a defense-in-depth gap that could be exploited by a compromised or misconfigured policy source.

    Severity Assessment

    • Impact: Medium — a malicious policy could grant overly permissive filesystem access (e.g., read_write: ["/"]) or run as root inside the sandbox. Impact is bounded by container isolation and namespace restrictions.
    • Exploitability: Low — the policy is set by the operator/CLI at sandbox creation time, not by untrusted sandbox users. Exploitation requires control over the gRPC API or policy YAML files.
    • Affected components:
      • crates/navigator-sandbox/src/policy.rs — TryFrom<ProtoSandboxPolicy> and From impls perform type conversion with no semantic validation
      • crates/navigator-server/src/grpc.rs — create_sandbox (line 72) and get_sandbox_policy (line 557) accept/return policy without validating content safety
      • crates/navigator-sandbox/src/sandbox/linux/landlock.rs — apply() uses paths verbatim from policy
      • crates/navigator-policy/src/lib.rs — parse_sandbox_policy() deserializes YAML with no safety constraints

    Attack Scenario

    1. Attacker gains access to the gRPC API (no auth middleware exists) or modifies a policy YAML file on disk
    2. Attacker creates a sandbox with run_as_user: "root" and read_write: ["/"], or uses path traversal patterns in filesystem paths
    3. The sandbox process runs as root with full filesystem access, weakening the security boundary. Combined with a container escape vulnerability, this could lead to host compromise.

    Remediation Plan

    1. Add a policy validation layer invoked at creation time (in create_sandbox and discover_policy_from_disk_or_default):
      • Reject or warn on run_as_user: "root" (or UID 0)
      • Validate filesystem paths: normalize with canonicalize() or reject relative/traversal paths (..), reject overly broad paths like /
      • Enforce maximum path count and string length limits
    2. Add path normalization in FilesystemPolicy::from(ProtoFilesystemPolicy) to canonicalize paths before they reach Landlock
    3. Consider an allowlist approach for run_as_user/run_as_group — only permit known sandbox-safe users
    4. Log warnings when policies are loaded that deviate from the restrictive default (e.g., missing process identity, unusually broad filesystem access)

    Additional Notes

    • The existing validate_static_fields_unchanged and validate_network_mode_unchanged functions show good precedent for server-side validation — the gap is that initial creation has no equivalent safety checks.
    • The gRPC server has no authentication middleware, which amplifies this issue: any client with network access to the server can set arbitrary policy. However, that is a separate concern (access control) from the policy validation gap described here.
    • The default restrictive_default_policy() is well-designed (sandbox user, limited paths, network blocked). The risk is primarily for custom/operator-supplied policies that bypass these defaults.
    • Severity is Medium rather than High because exploitation requires a privileged position (API access or file write), and container isolation provides a secondary boundary.
  2. johntmyers commented on Mar 5, 2026

    @johntmyers
    Collaborator

    🏗️ build-plan

    Implementation Plan

    Issue type: feat
    Complexity: Medium
    Confidence: High

    Summary

    Add a policy validation layer to navigator-policy that rejects or warns on unsafe policy content — specifically root process identity, path traversal in filesystem paths, overly broad paths (e.g. /), and unreasonable field sizes. Invoke this validation from all policy entry points: parse_sandbox_policy, load_sandbox_policy, create_sandbox, update_sandbox_policy, and discover_policy_from_disk_or_default.

    Scope

    • crates/navigator-policy/src/lib.rs: Add a validate_sandbox_policy(policy: &SandboxPolicy) -> Result<Vec<PolicyWarning>> function that checks process identity (reject UID 0 / "root"), filesystem path safety (reject relative paths, .. components, overly broad paths like /), and field size limits (max path count, max string length). Call it from parse_sandbox_policy and load_sandbox_policy.
    • crates/navigator-sandbox/src/policy.rs: Add validation in the TryFrom<ProtoSandboxPolicy> impl — normalize filesystem paths and reject unsafe values during conversion from proto to Rust types.
    • crates/navigator-server/src/grpc.rs: Add a validate_policy_safety call in create_sandbox (before persisting) and in update_sandbox_policy (before storing the new revision), returning Status::invalid_argument for hard violations. This mirrors the existing validate_static_fields_unchanged / validate_network_mode_unchanged pattern.
    • crates/navigator-sandbox/src/lib.rs: Add a validation call in discover_policy_from_disk_or_default to log warnings (and fall back to restrictive default) if the disk-loaded policy contains unsafe content.
    • e2e/python/test_policy_validation.py: Add e2e tests that validate server-side rejection of unsafe policies via the gRPC API.

    Implementation Steps

    1. Define validation types and core validator in navigator-policy: Add a PolicyViolation enum (with variants like RootProcessIdentity, PathTraversal, OverlyBroadPath, RelativePath, FieldTooLong) and a validate_sandbox_policy(&SandboxPolicy) -> Result<(), Vec<PolicyViolation>> function that checks: (a) run_as_user / run_as_group is not "root" or "0", (b) filesystem paths are absolute, contain no .. components, and are not overly broad (/), (c) path count and string length limits.
    2. Integrate validation into parse_sandbox_policy and load_sandbox_policy: After parsing YAML into proto, call validate_sandbox_policy and return an error if hard violations are found.
    3. Add path normalization in navigator-sandbox policy conversion: In FilesystemPolicy::from(ProtoFilesystemPolicy), normalize paths (collapse redundant separators, reject .. components) before storing them. This hardens the Landlock apply path.
    4. Add server-side validation in create_sandbox: Before persisting the sandbox, if spec.policy is Some, call validate_sandbox_policy and return Status::invalid_argument on violation. This is the primary defense for the gRPC API surface.
    5. Add server-side validation in update_sandbox_policy: After existing validate_static_fields_unchanged / validate_network_mode_unchanged checks, call validate_sandbox_policy on the new policy.
    6. Add validation in discover_policy_from_disk_or_default: After parse_sandbox_policy succeeds, validate the result. Log warnings and fall back to restrictive default on hard violations.
    7. Add unit tests in navigator-policy: Test that root user is rejected, path traversal is rejected, relative paths are rejected, overly broad paths are rejected, valid policies pass, and the restrictive default policy passes validation.
    8. Add unit tests in navigator-server: Test that create_sandbox and update_sandbox_policy reject policies with unsafe content (root user, bad paths).
    9. Add e2e tests in e2e/python/test_policy_validation.py: Using the existing e2e fixtures (sandbox, sandbox_client) from conftest.py, add tests that exercise the server-side validation end-to-end:
      • test_create_sandbox_rejects_root_user — create a sandbox with run_as_user: "root" and assert gRPC INVALID_ARGUMENT.
      • test_create_sandbox_rejects_path_traversal — use .. in filesystem paths and assert rejection.
      • test_create_sandbox_rejects_overly_broad_paths — use read_write: ["/"] and assert rejection.
      • test_update_policy_rejects_unsafe_content — create a sandbox with a valid policy, then call UpdateSandboxPolicy with unsafe content and assert INVALID_ARGUMENT.

    Test Plan

    • Unit tests: crates/navigator-policy/src/lib.rs — tests for the validate_sandbox_policy function covering all violation types and edge cases. crates/navigator-server/src/grpc.rs — tests for server-side rejection of unsafe policies (following the existing validate_static_fields_* test pattern).
    • Integration tests: N/A — the validation is purely in-process logic with no external dependencies.
    • E2e tests: e2e/python/test_policy_validation.py — tests that exercise the full gRPC path for create_sandbox and update_sandbox_policy rejection of unsafe policies. These follow the existing e2e patterns in test_sandbox_policy.py and use the sandbox/sandbox_client conftest fixtures.

    Risks & Open Questions

    • Backward compatibility: Existing deployments with custom policies that use run_as_user: root or broad paths like / will start failing. Consider whether to make root rejection a hard error or a logged warning. The plan defaults to hard error for create_sandbox and logged warning + fallback for disk-loaded policies.
    • Path canonicalization: Full canonicalize() requires the path to exist on the server, which it won't (paths are for the sandbox filesystem). Use lexical normalization (collapse //, reject ..) instead.
    • Scope of "overly broad" paths: Need to define the exact set of rejected paths. Suggest rejecting only / as read-write (read-only / may be acceptable in some configurations). Open to feedback.

    Documentation Impact

    • None expected — this is an internal safety layer. If hard rejections are added to the gRPC API, the error messages are self-descriptive.

    Revision 2 — added e2e tests (step 9) for server-side policy validation per feedback; updated scope, test plan
    Revision 1 — initial plan

  3. johntmyers commented on Mar 5, 2026

    @johntmyers
    Collaborator

    We could add a couple of e2e tests to validate server-side changes and rejection of policy updates that are violating.

  4. johntmyers commented on Mar 5, 2026

    @johntmyers
    Collaborator

    🏗️ build-from-issue-agent

    We could add a couple of e2e tests to validate server-side changes and rejection of policy updates that are violating.

    Agreed. The existing e2e test infrastructure in e2e/python/test_sandbox_policy.py already has comprehensive tests for policy behavior (L4, L7, SSRF, live policy updates), and the conftest.py fixtures provide sandbox and sandbox_client helpers that make this straightforward.

    The test_live_policy_update_and_logs test already calls UpdateSandboxPolicy via the gRPC stub, so we have a clear pattern for testing server-side rejection. Specifically, I'll add:

    1. test_create_sandbox_rejects_root_user — Calls sandbox() with run_as_user: "root" in the policy and asserts the gRPC error is INVALID_ARGUMENT.
    2. test_create_sandbox_rejects_path_traversal — Uses .. components in filesystem paths and asserts rejection.
    3. test_update_policy_rejects_unsafe_content — Creates a sandbox with a valid policy, then calls UpdateSandboxPolicy with a policy containing run_as_user: "root" or overly broad paths, and asserts INVALID_ARGUMENT.
    4. test_create_sandbox_rejects_overly_broad_paths — Uses read_write: ["/"] and asserts rejection.

    These will live in a new test file e2e/python/test_policy_validation.py to keep them separate from the existing network/proxy policy tests. I'll update the plan to reflect this.

  5. added a commit that references this issue on Mar 5, 2026
    ce6773c
  6. johntmyers commented on Mar 5, 2026

    @johntmyers
    Collaborator

    Implementation PR opened: #135

    Changes:

    • navigator-policy: Added PolicyViolation enum, validate_sandbox_policy(), normalize_path(), constants for limits, and 13 unit tests
    • navigator-sandbox: Path normalization in FilesystemPolicy::from(), validation in discover_policy_from_path() with fallback to restrictive default, 1 unit test
    • navigator-server: validate_policy_safety() helper called in create_sandbox and update_sandbox_policy (returns INVALID_ARGUMENT), 4 unit tests
    • e2e/python/test_policy_validation.py: 4 e2e tests covering root user rejection, path traversal rejection, overly broad paths, and update policy rejection

    All local pre-commit checks pass (rust:check, rust:lint, rust:format:check, test:rust, python:format:check, python:proto, helm:lint).

  7. added a commit that references this issue on Mar 9, 2026
    3e260b6
  8. added a commit that references this issue on Mar 16, 2026
    024150e
  9. added a commit that references this issue on Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions