Skip to content

Log boundary relaxations with secure-default warnings - #726

Open
Gudge (MGudgin) wants to merge 1 commit into
mainfrom
user/gudge/trust-model-relaxation-logging
Open

Log boundary relaxations with secure-default warnings#726
Gudge (MGudgin) wants to merge 1 commit into
mainfrom
user/gudge/trust-model-relaxation-logging

Conversation

@MGudgin

@MGudgin Gudge (MGudgin) commented Aug 1, 2026

Copy link
Copy Markdown
Member

This PR adds deterministic security warnings for sandbox policy settings that relax secure defaults, making weakened boundaries visible through CLI diagnostics and Rust SDK warning results. It changes logging only; policy defaults and enforcement remain unchanged.

Details

  • Centralize effective network, UI, ProcessContainer, and Seatbelt relaxation detection after policy parsing, gated by the selected backend.
  • Suppress inert and parser-injected settings, name effective capabilities, use stable wire values, and sanitize caller-controlled log text.
  • Emit warnings through Logger::warning_line so successful CLI runs cannot silently discard them.
  • Retain parser warnings on SandboxRequest and merge them with backend and spawn-time warnings for Sandbox::warnings() and Output::warnings().

Tests

  • cargo fmt --all -- --check, cargo check --workspace --all-targets, and cargo clippy --workspace --all-targets -- -D warnings passed.
  • cargo test --workspace passed: 2,003 tests across 80 suites; 24 host-dependent tests ignored.
  • Manual wxc-exec verification confirmed relaxed policies emit the expected SECURITY: boundary relaxed: lines without --debug, while secure-default and learning-mode-only policies stay silent.

Related pull requests

Stack, merge bottom-up:

  1. This PR - boundary-relaxation logging
  2. Make seatbelt.profileOverride dev-only (rejected by shipped builds) #727 - reject seatbelt.profileOverride in shipped builds
  3. Document the trust model and the outer-clamp design #728 - document the trust model and outer-clamp design

Copilot AI balanced review requested due to automatic review settings August 1, 2026 00:36
@MGudgin
Gudge (MGudgin) requested a review from a team as a code owner August 1, 2026 00:36
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds centralized logging for sandbox policy settings that relax security boundaries.

Changes:

  • Detects network, UI, ProcessContainer, and Seatbelt relaxations.
  • Sanitizes free-form log values.
  • Adds five unit tests for warning behavior.
Show a summary per file
File Description
src/core/wxc_common/src/config_parser.rs Adds boundary-relaxation diagnostics and tests.

Review details

  • Files reviewed: 1/1 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment on lines +820 to +826
// ProcessContainer capabilities (effective regardless of ui.disable).
if !policy.capabilities.is_empty() {
logger.log_line(&format!(
"{P} processContainer.capabilities ({} cap(s))",
policy.capabilities.len()
));
}
};
}

log_boundary_relaxations(&policy, seatbelt.as_ref(), logger);
Comment on lines +749 to +752
fn log_boundary_relaxations(
policy: &ContainerPolicy,
seatbelt: Option<&SeatbeltConfig>,
logger: &mut Logger,
Comment on lines +777 to +779
// UI. Clipboard, injection, windows, and the BaseProcess desktop knobs are
// inert while UI is disabled; only warn when ui.disable=false opens them.
// `ime` and capabilities stay effective regardless and are reported below.
policy.allowed_hosts.len()
));
}
// blockedHosts is intentionally not reported: it only subtracts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Hyperlight, a non-empty blocked_hosts returns BlockList ("rest allowed") before the default_network_policy == Block -> None (networking-disabled) check; on NanVix, any host list enables networking "regardless of defaultPolicy." So, under  defaultPolicy=block,  blockedHosts flips networking OFF->ON- a real relaxation this feature should log?

"{P} processContainer.ui.desktopSystemControl=true"
));
}
if ui.system_settings != "none" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enforcement (ui_policy.rs) only relaxes for all / parameters / display; any unrecognized value falls through to default-deny. Warning on != "none" emits a false boundary relaxed line for inert/garbage values. Gate on matches! (ui.system_settings.as_str(), "all" | "parameters" | "display") 

'\t' => out.push_str("\\t"),
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
c if c.is_control() => out.push_str(&format!("\\u{{{:04x}}}", c as u32)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

char::is_control() only covers category Cc; U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR) are not control chars and pass through unescaped?

}

// ProcessContainer capabilities (effective regardless of ui.disable).
if !policy.capabilities.is_empty() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processContainer.learningMode=true injects learningModeLogging into policy.capabilities, which is observability-only- the note at :833 states enforcement is unchanged. This block then falsely reports it as a capabilities relaxation?

// redundant on Windows (where UI-disabled forces clipboard blocks) but never
// misses a real relaxation.
if policy.ui.clipboard != ClipboardPolicy::None {
logger.log_line(&format!("{P} ui.clipboard={:?}", policy.ui.clipboard));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{:?}  writes internal variant names (All / Read), not the stable lowercase wire form, so the persisted audit line and the test (ui.clipboard=All) break if the enum is renamed?

Copilot AI review requested due to automatic review settings August 3, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (5)

src/core/wxc_common/src/config_parser.rs:820

  • Bubblewrap is also missing from this check when network.enforcementMode is the default capabilities: bwrap_command.rs:137-149 drops --unshare-net whenever any host list exists, while bwrap_runner.rs:480-492 installs iptables only for firewall/both. Therefore a deny-by-default Bubblewrap config containing only blockedHosts opens shared networking but emits no blockedHosts security warning. Include this mode and add it to the regression test.
    let list_implies_networking = matches!(
        containment,
        ContainmentBackend::MicroVm | ContainmentBackend::Hyperlight
    );

src/core/wxc_common/src/config_parser.rs:798

  • allowLocalNetwork is only consumed by the Seatbelt profile builder; all other backends ignore this domain field. (The Windows SDK separately translates its policy option into privateNetworkClientServer.) A direct LXC/WSLC/ProcessContainer config therefore gets a “boundary relaxed” line without changing enforcement. Restrict this field-level warning to Seatbelt; Windows SDK requests will still be covered by the effective capability warning.
    if policy.allow_local_network {
        logger.warning_line(&format!("{P} network.allowLocalNetwork=true"));
    }

src/core/wxc_common/src/config_parser.rs:857

  • These UI warnings are emitted for every containment backend, but only ProcessContainer and Seatbelt read policy.ui (the only backend usages are in the AppContainer/BaseContainer and Seatbelt crates). For example, a valid LXC config with ui.disable=false now reports “windows allowed” although LXC ignores the setting and no boundary changes. Gate the UI diagnostics on an active backend that actually applies them so the audit stream does not report nonexistent relaxations.
    if !policy.ui.disable {
        logger.warning_line(&format!("{P} ui.disable=false (windows allowed)"));
        if policy.ui.injection {
            logger.warning_line(&format!("{P} ui.injection=true"));
        }

src/core/wxc_common/src/config_parser.rs:825

  • The PR description says blockedHosts is deliberately never reported because it is purely subtractive, but this branch correctly reports it for backends where a list enables networking. Please update the description (and its stated test count/list) so reviewers and future audit documentation match the implemented behavior.
    if list_implies_networking
        && !policy.blocked_hosts.is_empty()
        && policy.default_network_policy == NetworkPolicy::Block
    {
        logger.warning_line(&format!(

src/core/wxc_common/src/config_parser.rs:911

  • Learning mode is not the only synthesized capability. Both Windows SDK builders derive internetClient and privateNetworkClientServer from the network policy (sdk/node/src/sandbox.ts:142-146 and mxc_engine/src/policy.rs:747-755). Consequently an SDK allowOutbound request emits both the network warning and a processContainer.capabilities warning that is incorrectly attributed to a caller-requested capability. Exclude already-accounted derived network capabilities or preserve capability origin through parsing.
        let requested_capabilities = policy
            .capabilities
            .iter()
            .filter(|c| {
                !c.eq_ignore_ascii_case(LEARNING_MODE_LOGGING_CAPABILITY)
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings August 3, 2026 20:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (6)

src/core/wxc_common/src/config_parser.rs:799

  • allowedHosts only opens connectivity under defaultPolicy=block. Under allow it is either redundant (LXC/Bubblewrap/WSLc/Seatbelt) or restrictive (Hyperlight/NanVix), so labeling it as another boundary relaxation is a false audit entry. Match the blockedHosts logic by considering the effective default.
    if !policy.allowed_hosts.is_empty() {

src/core/wxc_common/src/config_parser.rs:791

  • On Hyperlight/NanVix an allowlist overrides defaultPolicy, so defaultPolicy=allow plus allowedHosts produces an allowlist—not “network open by default.” This currently emits a false relaxation line in addition to the allowlist line. Suppress the default-policy warning when those backends have an allowlist, or describe the effective policy instead.
    if policy.default_network_policy == NetworkPolicy::Allow {

src/core/wxc_common/src/config_parser.rs:847

  • Clipboard warnings are emitted for every backend, but only ProcessContainer and Seatbelt consume policy.ui; Bubblewrap, LXC, WSLc, and the VM backends ignore it. For those successful runs this records a boundary relaxation that never occurred, contrary to the active-backend audit contract.
    if policy.ui.clipboard != ClipboardPolicy::None {

src/core/wxc_common/src/config_parser.rs:853

  • This generic UI branch also runs for backends that never inspect policy.ui, so e.g. containment=microvm with ui.disable=false reports “windows allowed” although execution is unchanged. Gate the branch to ProcessContainer/Seatbelt so the audit describes the selected sandbox.
    if !policy.ui.disable {

src/core/wxc_common/src/config_parser.rs:855

  • For Seatbelt, injection=true only removes an explicit HID deny; deny-default still blocks HID unless guiAccess emits the broad (allow iokit-open). Thus ui.disable=false, injection=true, guiAccess=false is inert but is logged as a relaxation. Keep the unconditional behavior for ProcessContainer, but require effective guiAccess for Seatbelt.
        if policy.ui.injection {

src/core/wxc_common/src/config_parser.rs:783

  • The PR promises a warning for every setting that opens a boundary, but this helper cannot inspect experimental.wslc.portMappings. A nonempty mapping is passed to WslcSetContainerSettingsPortMappings and forwards a Windows host port (src/backends/wslc/common/src/wsl_container_runner.rs:1093-1125), yet it produces no SECURITY line. Pass the active WSLc config into this audit and warn for nonempty mappings.
fn log_boundary_relaxations(
    policy: &ContainerPolicy,
    seatbelt: Option<&SeatbeltConfig>,
    containment: &ContainmentBackend,
    logger: &mut Logger,
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings August 3, 2026 20:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (4)

src/core/wxc_common/src/config_parser.rs:813

  • allowedHosts is only a relaxation relative to deny-by-default. With defaultPolicy=allow, LXC still ends its chain in ACCEPT (network_iptables.rs:226-232), while Hyperlight turns the request into an AllowList (hyperlight/common/src/lib.rs:327-341), so this line produces a false security warning for a redundant or stricter setting. Only report this field when the effective baseline is block (in addition to backend support gating).
    if !policy.allowed_hosts.is_empty() {
        logger.warning_line(&format!(
            "{P} network.allowedHosts ({} host(s))",
            policy.allowed_hosts.len()
        ));
    }

src/core/wxc_common/src/config_parser.rs:963

  • When profileOverride is present, build_profile_with_proxy returns the override immediately (profile_builder.rs:43-49) and never applies generated-profile network, UI, keychain, or extra-Mach settings. This function has already warned for those ignored settings and continues to warn for keychain/extra lookups below, so the audit stream does not describe the profile that runs. Detect the override before profile-derived checks and suppress those diagnostics, while retaining the profileOverride warning and the independent launchMethod warning.
        if sb.profile_override.is_some() {
            logger.warning_line(&format!(
                "{P} seatbelt.profileOverride (generated profile bypassed)"
            ));
        }

src/core/wxc_common/src/config_parser.rs:870

  • On ProcessContainer, ui.disable=true forces both clipboard restrictions regardless of ui.clipboard (ui_policy.rs:53-65), so this condition emits a “boundary relaxed” warning when no Windows boundary changed. Keep the unconditional clipboard warning only for Seatbelt; on ProcessContainer require UI to be enabled.
    if honors_ui && policy.ui.clipboard != ClipboardPolicy::None {
        logger.warning_line(&format!(
            "{P} ui.clipboard={}",
            policy.ui.clipboard.wire_name()
        ));

src/core/wxc_common/src/config_parser.rs:796

  • This also fires for backends that reject the setting rather than relaxing anything. Windows Sandbox rejects defaultPolicy=allow (windows_sandbox/lifecycle/src/policy.rs:52-58), and IsolationSession rejects every non-block policy (isolation_session/common/src/policy.rs:70-80); because this runs during parsing, the log claims “boundary relaxed” before those backends fail validation. The same issue affects the unconditional host-list and proxy warnings below. Gate each network warning on the active backend's supported/effective policy, or emit these diagnostics only after backend validation succeeds.

This issue also appears on line 808 of the same file.

    if policy.default_network_policy == NetworkPolicy::Allow {
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@MGudgin
Gudge (MGudgin) force-pushed the user/gudge/trust-model-relaxation-logging branch from 45177a7 to 31d6420 Compare August 3, 2026 21:40
Copilot AI review requested due to automatic review settings August 8, 2026 20:23
@MGudgin
Gudge (MGudgin) force-pushed the user/gudge/trust-model-relaxation-logging branch from 31d6420 to 3f543f5 Compare August 8, 2026 20:23
@MGudgin
Gudge (MGudgin) requested a review from a team August 8, 2026 20:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

src/core/wxc_common/src/config_parser.rs:796

  • This reports a relaxation for backends that reject this policy. Windows Sandbox rejects defaultPolicy=allow in windows_sandbox/lifecycle/src/policy.rs:52-58, and IsolationSession rejects every non-Block policy in isolation_session/common/src/policy.rs:70-79, so these requests open no boundary but still emit a forged-looking SECURITY audit entry before backend validation fails. Gate the warning to backends that can actually honor Allow.
    if policy.default_network_policy == NetworkPolicy::Allow {

src/core/wxc_common/src/config_parser.rs:812

  • This unconditional audit claim also fires when allowedHosts does not relax anything. With defaultPolicy=allow, LXC appends a terminal ACCEPT after the host rules, WSLC explicitly ignores allowedHosts in Allow mode, and Hyperlight/Seatbelt can only narrow or leave an already-open policy unchanged. Conversely, Bubblewrap's default capabilities mode makes an allowlist open the entire unfiltered host namespace, but this generic message does not disclose that severity. Gate this on the effective backend/default-policy combination and report the fail-open cases explicitly so each boundary relaxed line describes the sandbox that will actually run.
    if !policy.allowed_hosts.is_empty() {
        logger.warning_line(&format!(
            "{P} network.allowedHosts ({} host(s))",
            policy.allowed_hosts.len()
        ));
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

This PR adds deterministic security warnings for sandbox policy settings that relax secure defaults and exposes them through CLI and Rust SDK warning channels.

Details

* Evaluate effective network, UI, ProcessContainer, and Seatbelt relaxations for the selected backend.
* Suppress inert and parser-injected settings, sanitize caller-controlled values, and use stable wire names in audit output.
* Preserve parser warnings on SandboxRequest and merge them with backend and spawn-time warnings without duplicates.

Tests

* `cargo fmt --all -- --check` passed.
* `cargo check --workspace --all-targets` passed.
* `cargo clippy --workspace --all-targets -- -D warnings` passed.
* `cargo test --workspace` passed: 2,003 tests across 80 suites; 24 host-dependent tests ignored.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c98a35b9-0503-4e95-9f0c-b2a0d7d13174
Copilot AI review requested due to automatic review settings August 8, 2026 20:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (5)

src/core/wxc_common/src/config_parser.rs:796

  • This emits a “boundary relaxed” audit event for Windows Sandbox and IsolationSession even though both backend validators reject defaultPolicy=allow; no relaxed sandbox can run. Gate this warning off for those backends so the audit stream describes effective policy rather than a rejected request.
    if policy.default_network_policy == NetworkPolicy::Allow {

src/core/wxc_common/src/config_parser.rs:850

  • Proxy support is not cross-backend: LXC never consumes network_proxy, and Hyperlight, MicroVM, Windows Sandbox, and IsolationSession reject it. This unconditional line therefore claims an effective relaxation for ignored or rejected policy. Restrict the warning to backends that actually implement proxy handling (and account for ProcessContainer contract/version constraints).
    if policy.network_proxy.is_enabled() {

src/core/wxc_common/src/config_parser.rs:808

  • With defaultPolicy=allow, allowedHosts does not open a boundary: Hyperlight/NanVix turn it into a restrictive allowlist, while backends such as LXC already allow the unlisted destinations via their default action. The unconditional warning therefore labels a tightening or no-op as a relaxation; only emit it under the deny-by-default policy.
    if !policy.allowed_hosts.is_empty() {

src/core/mxc_engine/src/policy.rs:702

  • policy_warnings is cached before the request's public policy mutators run. set_seatbelt_extra_mach_lookups and set_seatbelt_keychain_access modify inner afterward, so enabling either on a secure request produces no SDK warning, while disabling a previously enabled setting can leave a stale warning. Recompute warnings from the final request at spawn time, or keep this cache synchronized in every boundary-affecting setter.
    /// Security warnings emitted while the policy was parsed and validated.
    /// Spawn-time warnings are merged with these before the public SDK sees the
    /// resulting sandbox handle.
    pub(crate) policy_warnings: Vec<String>,

src/core/wxc_common/src/config_parser.rs:866

  • This reports an inert setting on ProcessContainer when ui.disable is true: resolve_ui_restrictions unconditionally blocks both clipboard directions in that state. Seatbelt does honor clipboard independently, so keep the unconditional behavior only there and require UI to be enabled on ProcessContainer.
    if honors_ui && policy.ui.clipboard != ClipboardPolicy::None {
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants