mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-17 05:31:16 +08:00
* feat(policy): add validation layer to reject unsafe sandbox policies Add policy validation that checks for root process identity, path traversal sequences, overly broad filesystem paths, and exceeding filesystem rule limits. Validation runs at three entry points: disk-loaded YAML policies (fallback to restrictive default on violation), gRPC CreateSandbox, and gRPC UpdateSandboxPolicy (returns INVALID_ARGUMENT). Filesystem paths are normalized before storage to collapse traversal components. Closes #33 * fix(e2e): correct policy update test to match immutable field behavior The update policy test was asserting on validation errors for fields (process, filesystem) that are immutable on live sandboxes. The server rejects changes to these fields before validation runs. Updated the test to verify the immutability guard instead. --------- Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
165 lines
5.4 KiB
Python
165 lines
5.4 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""E2E tests for server-side policy safety validation.
|
|
|
|
These tests verify that the gRPC server rejects sandbox creation and policy
|
|
updates that contain unsafe content (root process identity, path traversal,
|
|
overly broad filesystem paths).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import grpc
|
|
import pytest
|
|
|
|
from navigator._proto import datamodel_pb2, navigator_pb2, sandbox_pb2
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
from navigator import Sandbox, SandboxClient
|
|
|
|
|
|
# =============================================================================
|
|
# Policy helpers
|
|
# =============================================================================
|
|
|
|
_SAFE_FILESYSTEM = sandbox_pb2.FilesystemPolicy(
|
|
include_workdir=True,
|
|
read_only=["/usr", "/lib", "/etc", "/app", "/var/log"],
|
|
read_write=["/sandbox", "/tmp"],
|
|
)
|
|
_SAFE_LANDLOCK = sandbox_pb2.LandlockPolicy(compatibility="best_effort")
|
|
_SAFE_PROCESS = sandbox_pb2.ProcessPolicy(run_as_user="sandbox", run_as_group="sandbox")
|
|
|
|
|
|
def _safe_policy() -> sandbox_pb2.SandboxPolicy:
|
|
"""Build a safe baseline policy for testing."""
|
|
return sandbox_pb2.SandboxPolicy(
|
|
version=1,
|
|
filesystem=_SAFE_FILESYSTEM,
|
|
landlock=_SAFE_LANDLOCK,
|
|
process=_SAFE_PROCESS,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Tests
|
|
# =============================================================================
|
|
|
|
|
|
def test_create_sandbox_rejects_root_user(
|
|
sandbox_client: SandboxClient,
|
|
) -> None:
|
|
"""Server rejects CreateSandbox with run_as_user='root'."""
|
|
policy = sandbox_pb2.SandboxPolicy(
|
|
version=1,
|
|
filesystem=_SAFE_FILESYSTEM,
|
|
landlock=_SAFE_LANDLOCK,
|
|
process=sandbox_pb2.ProcessPolicy(
|
|
run_as_user="root",
|
|
run_as_group="sandbox",
|
|
),
|
|
)
|
|
spec = datamodel_pb2.SandboxSpec(policy=policy)
|
|
|
|
stub = sandbox_client._stub
|
|
with pytest.raises(grpc.RpcError) as exc_info:
|
|
stub.CreateSandbox(navigator_pb2.CreateSandboxRequest(name="", spec=spec))
|
|
|
|
assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT
|
|
assert "root" in exc_info.value.details().lower()
|
|
|
|
|
|
def test_create_sandbox_rejects_path_traversal(
|
|
sandbox_client: SandboxClient,
|
|
) -> None:
|
|
"""Server rejects CreateSandbox with '..' in filesystem paths."""
|
|
policy = sandbox_pb2.SandboxPolicy(
|
|
version=1,
|
|
filesystem=sandbox_pb2.FilesystemPolicy(
|
|
include_workdir=True,
|
|
read_only=["/usr/../etc/shadow"],
|
|
read_write=["/tmp"],
|
|
),
|
|
landlock=_SAFE_LANDLOCK,
|
|
process=_SAFE_PROCESS,
|
|
)
|
|
spec = datamodel_pb2.SandboxSpec(policy=policy)
|
|
|
|
stub = sandbox_client._stub
|
|
with pytest.raises(grpc.RpcError) as exc_info:
|
|
stub.CreateSandbox(navigator_pb2.CreateSandboxRequest(name="", spec=spec))
|
|
|
|
assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT
|
|
assert "traversal" in exc_info.value.details().lower()
|
|
|
|
|
|
def test_create_sandbox_rejects_overly_broad_paths(
|
|
sandbox_client: SandboxClient,
|
|
) -> None:
|
|
"""Server rejects CreateSandbox with read_write=['/']."""
|
|
policy = sandbox_pb2.SandboxPolicy(
|
|
version=1,
|
|
filesystem=sandbox_pb2.FilesystemPolicy(
|
|
include_workdir=True,
|
|
read_only=["/usr"],
|
|
read_write=["/"],
|
|
),
|
|
landlock=_SAFE_LANDLOCK,
|
|
process=_SAFE_PROCESS,
|
|
)
|
|
spec = datamodel_pb2.SandboxSpec(policy=policy)
|
|
|
|
stub = sandbox_client._stub
|
|
with pytest.raises(grpc.RpcError) as exc_info:
|
|
stub.CreateSandbox(navigator_pb2.CreateSandboxRequest(name="", spec=spec))
|
|
|
|
assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT
|
|
assert "broad" in exc_info.value.details().lower()
|
|
|
|
|
|
def test_update_policy_rejects_immutable_fields(
|
|
sandbox: Callable[..., Sandbox],
|
|
sandbox_client: SandboxClient,
|
|
) -> None:
|
|
"""UpdateSandboxPolicy rejects changes to immutable policy fields.
|
|
|
|
Both process and filesystem policies are applied at sandbox startup and
|
|
cannot be changed on a live sandbox. This test verifies that the server
|
|
rejects such updates, which also prevents unsafe content from being
|
|
introduced via policy updates to these fields.
|
|
"""
|
|
safe_policy = _safe_policy()
|
|
spec = datamodel_pb2.SandboxSpec(policy=safe_policy)
|
|
|
|
with sandbox(spec=spec, delete_on_exit=True) as sb:
|
|
sandbox_name = sb.sandbox.name
|
|
stub = sandbox_client._stub
|
|
|
|
# Try to update with a modified filesystem policy (immutable field)
|
|
unsafe_policy = sandbox_pb2.SandboxPolicy(
|
|
version=1,
|
|
filesystem=sandbox_pb2.FilesystemPolicy(
|
|
include_workdir=True,
|
|
read_only=["/usr/../etc/shadow"],
|
|
read_write=["/tmp"],
|
|
),
|
|
landlock=_SAFE_LANDLOCK,
|
|
process=_SAFE_PROCESS,
|
|
)
|
|
|
|
with pytest.raises(grpc.RpcError) as exc_info:
|
|
stub.UpdateSandboxPolicy(
|
|
navigator_pb2.UpdateSandboxPolicyRequest(
|
|
name=sandbox_name,
|
|
policy=unsafe_policy,
|
|
)
|
|
)
|
|
|
|
assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT
|
|
assert "cannot be changed" in exc_info.value.details().lower()
|