diff --git a/src/core/wxc_common/tests/proxy_env_spec.rs b/src/core/wxc_common/tests/proxy_env_spec.rs index 238f0db52..b68ddf96a 100644 --- a/src/core/wxc_common/tests/proxy_env_spec.rs +++ b/src/core/wxc_common/tests/proxy_env_spec.rs @@ -194,6 +194,36 @@ fn cooperative_env_scrubs_all_caller_supplied_proxy_keys() { assert_eq!(value_for(&result, "ftp_proxy"), Some(PROXY_URL)); } +// Protects clients (b), (c), and (d): the three key-set constants are the +// contract, so this derives the expected state of every managed key from them +// instead of naming keys by hand. A hand-written per-key expectation can +// silently disagree with the constant that actually drives the behavior -- +// which is how an assertion that FTP_PROXY stays absent survived FTP_PROXY +// being added to PROXY_SET_KEYS, leaving the suite asserting both at once. +#[test] +fn every_managed_key_lands_in_the_state_its_constants_dictate() { + let hostile = "http://attacker.example:9999"; + let caller: Vec = PROXY_ENV_KEYS + .iter() + .map(|key| format!("{key}={hostile}")) + .collect(); + + let result = apply_cooperative_proxy_env(&caller, PROXY_URL); + + for key in PROXY_ENV_KEYS { + let expected = if PROXY_SET_KEYS.contains(key) { + Some(PROXY_URL) + } else if PROXY_NEUTRALIZE_KEYS.contains(key) { + Some("") + } else { + None + }; + assert_eq!(value_for(&result, key), expected, "managed key {key}"); + } + + assert!(!result.iter().any(|entry| entry.contains(hostile))); +} + // Protects client (d): duplicate and mixed-case proxy keys are an obvious // evasion attempt. Neither the second copy nor an unusual casing survives with // the workload's value. diff --git a/tests/configs/wslc_filesystem.json b/tests/configs/wslc_filesystem.json index d9c63abc7..d655fba6a 100644 --- a/tests/configs/wslc_filesystem.json +++ b/tests/configs/wslc_filesystem.json @@ -3,11 +3,11 @@ "containerId": "wslc-filesystem-test", "containment": "wslc", "process": { - "commandLine": "set -e; ls -la /mnt/c/workspace >/dev/null && echo 'PASS: filesystem mount visible' || { echo 'FAIL: /mnt/c/workspace not visible'; exit 1; }; cpus=$(nproc); echo \"CPU_COUNT=$cpus\"; [ \"$cpus\" = \"2\" ] && echo 'PASS: cpuCount enforced (nproc=2)' || { echo \"FAIL: expected nproc=2, got $cpus\"; exit 1; }; mem_kb=$(awk '/MemTotal:/ {print $2}' /proc/meminfo); mem_mb=$((mem_kb/1024)); echo \"MEM_TOTAL_MB=$mem_mb\"; [ $mem_mb -ge 820 ] && [ $mem_mb -le 1075 ] && echo 'PASS: memoryMb enforced (~1024MB, kernel-visible 80-105%)' || { echo \"FAIL: expected 820-1075MB, got ${mem_mb}MB\"; exit 1; }", - "cwd": "C:\\workspace" + "commandLine": "set -e; ls -la /mnt/c/wslcfs >/dev/null && echo 'PASS: filesystem mount visible' || { echo 'FAIL: /mnt/c/wslcfs not visible'; exit 1; }; cpus=$(nproc); echo \"CPU_COUNT=$cpus\"; [ \"$cpus\" = \"2\" ] && echo 'PASS: cpuCount enforced (nproc=2)' || { echo \"FAIL: expected nproc=2, got $cpus\"; exit 1; }; mem_kb=$(awk '/MemTotal:/ {print $2}' /proc/meminfo); mem_mb=$((mem_kb/1024)); echo \"MEM_TOTAL_MB=$mem_mb\"; [ $mem_mb -ge 820 ] && [ $mem_mb -le 1075 ] && echo 'PASS: memoryMb enforced (~1024MB, kernel-visible 80-105%)' || { echo \"FAIL: expected 820-1075MB, got ${mem_mb}MB\"; exit 1; }", + "cwd": "C:\\wslcfs" }, "filesystem": { - "readwritePaths": ["C:\\workspace"] + "readwritePaths": ["C:\\wslcfs"] }, "network": { "defaultPolicy": "allow" diff --git a/tests/configs/wslc_network_proxy.json b/tests/configs/wslc_network_proxy.json index 8556b438e..a96793909 100644 --- a/tests/configs/wslc_network_proxy.json +++ b/tests/configs/wslc_network_proxy.json @@ -3,8 +3,8 @@ "containerId": "wslc-network-proxy", "containment": "wslc", "process": { - "commandLine": "set -e; ( while true; do printf 'HTTP/1.0 200 OK\\r\\nContent-Length: 9\\r\\nConnection: close\\r\\n\\r\\nPROXY_HIT' | nc -l -p 8888 -w 2 >/dev/null 2>&1; done ) & sleep 1; echo \"PROXY_ENV HTTP_PROXY=[$HTTP_PROXY] http_proxy=[$http_proxy] NO_PROXY=[$NO_PROXY] no_proxy=[$no_proxy]\"; BODY=$(wget -q -O - http://example.com/ 2>/dev/null || true); echo \"CLIENT_GOT=[$BODY]\"; case \"$BODY\" in *PROXY_HIT*) echo WSLC_PROXY_FUNCTIONAL_OK ;; *) echo WSLC_PROXY_FAIL ;; esac", - "env": ["HTTP_PROXY=http://attacker.invalid:1", "http_proxy=http://attacker.invalid:1", "NO_PROXY=*", "no_proxy=attacker.invalid"] + "commandLine": "set -e; ( while true; do printf 'HTTP/1.0 200 OK\\r\\nContent-Length: 9\\r\\nConnection: close\\r\\n\\r\\nPROXY_HIT' | nc -l -p 8888 -w 2 >/dev/null 2>&1; done ) & sleep 1; echo \"PROXY_ENV HTTP_PROXY=[$HTTP_PROXY] http_proxy=[$http_proxy] FTP_PROXY=[$FTP_PROXY] ftp_proxy=[$ftp_proxy] NO_PROXY=[$NO_PROXY] no_proxy=[$no_proxy]\"; BODY=$(wget -q -O - http://example.com/ 2>/dev/null || true); echo \"CLIENT_GOT=[$BODY]\"; case \"$BODY\" in *PROXY_HIT*) echo WSLC_PROXY_FUNCTIONAL_OK ;; *) echo WSLC_PROXY_FAIL ;; esac", + "env": ["HTTP_PROXY=http://attacker.invalid:1", "http_proxy=http://attacker.invalid:1", "FTP_PROXY=http://attacker.invalid:1", "ftp_proxy=http://attacker.invalid:1", "NO_PROXY=*", "no_proxy=attacker.invalid"] }, "network": { "defaultPolicy": "allow", diff --git a/tests/configs/wslc_readonly_mount.json b/tests/configs/wslc_readonly_mount.json index 6a069d0ac..5ad6d429e 100644 --- a/tests/configs/wslc_readonly_mount.json +++ b/tests/configs/wslc_readonly_mount.json @@ -3,10 +3,10 @@ "containerId": "wslc-readonly-mount", "containment": "wslc", "process": { - "commandLine": "cat /mnt/c/workspace/test.txt && echo 'Read succeeded' && echo 'write test' > /mnt/c/workspace/readonly_write_test.txt && echo 'Write succeeded (unexpected)' || echo 'Write blocked (expected for readonly)'" + "commandLine": "cat /mnt/c/wslcro/test.txt && echo 'Read succeeded' && echo 'write test' > /mnt/c/wslcro/readonly_write_test.txt && echo 'Write succeeded (unexpected)' || echo 'Write blocked (expected for readonly)'" }, "filesystem": { - "readonlyPaths": ["C:\\workspace"] + "readonlyPaths": ["C:\\wslcro"] }, "network": { "defaultPolicy": "block" diff --git a/tests/scripts/run_wslc_all_tests.ps1 b/tests/scripts/run_wslc_all_tests.ps1 index 9c2a6cb49..4e4958f50 100644 --- a/tests/scripts/run_wslc_all_tests.ps1 +++ b/tests/scripts/run_wslc_all_tests.ps1 @@ -215,10 +215,39 @@ $null = $results.Add((Run-WslcTest "wslc_stderr.json" -OutputContains "stdout me $null = $results.Add((Run-WslcTest "wslc_large_output.json")) Write-Host "`n--- Filesystem Tests ---" -ForegroundColor Cyan -# wslc_filesystem.json also asserts cpuCount + memoryMb enforcement via nproc and /proc/meminfo. -$null = $results.Add((Run-WslcTest "wslc_filesystem.json" ` - -OutputMatches "(?s)PASS: filesystem mount visible.*PASS: cpuCount enforced.*PASS: memoryMb enforced")) -$null = $results.Add((Run-WslcTest "wslc_readonly_mount.json" -OutputContains "Read succeeded")) + +# Fixed paths must match tests\configs\wslc_filesystem.json and +# tests\configs\wslc_readonly_mount.json. +$fsFixtureDir = "C:\wslcfs" +$readonlyFixtureDir = "C:\wslcro" +$readonlyFixture = Join-Path $readonlyFixtureDir "test.txt" + +function Remove-FilesystemFixtures { + Remove-Item -Recurse -Force $fsFixtureDir -ErrorAction SilentlyContinue + Remove-Item -Recurse -Force $readonlyFixtureDir -ErrorAction SilentlyContinue +} + +# Both filesystem configs mount a host directory the suite owns outright, so a +# run needs nothing hand-made and never touches C:\workspace, where the +# prerequisites above tell the developer to keep alpine.tar. wslc_filesystem +# only needs its mount target to exist; wslc_readonly_mount also reads +# test.txt, seeded the way the LXC and bubblewrap suites seed theirs +# (run_lxc_filesystem_test.sh:21, run_bwrap_filesystem_test.sh:25). The +# finally removes both roots, including the probe file a wrongly-writable +# mount would leave behind. +Remove-FilesystemFixtures +try { + $null = New-Item -ItemType Directory -Path $fsFixtureDir -Force + $null = New-Item -ItemType Directory -Path $readonlyFixtureDir -Force + Set-Content -Path $readonlyFixture -Value "test content" -Encoding ascii + + # wslc_filesystem.json also asserts cpuCount + memoryMb enforcement via nproc and /proc/meminfo. + $null = $results.Add((Run-WslcTest "wslc_filesystem.json" ` + -OutputMatches "(?s)PASS: filesystem mount visible.*PASS: cpuCount enforced.*PASS: memoryMb enforced")) + $null = $results.Add((Run-WslcTest "wslc_readonly_mount.json" -OutputContains "Read succeeded")) +} finally { + Remove-FilesystemFixtures +} Write-Host "`n--- Object Validation Tests ---" -ForegroundColor Cyan # Object-based validation (roadmap D6): a directory under readwritePaths and a diff --git a/tests/scripts/run_wslc_proxy_test.ps1 b/tests/scripts/run_wslc_proxy_test.ps1 index d8f6ff302..9da6300c0 100644 --- a/tests/scripts/run_wslc_proxy_test.ps1 +++ b/tests/scripts/run_wslc_proxy_test.ps1 @@ -96,6 +96,15 @@ if ($pass -and ($childLine -match 'attacker\.invalid')) { $reason = "caller-supplied proxy env var leaked to the child (attacker.invalid not scrubbed)" } +# The FTP family is set to the proxy as well, so an image-baked ftp_proxy +# cannot send FTP traffic somewhere the operator did not choose. Matched +# case-sensitively: -notmatch would let the upper-case entry satisfy both +# checks and leave the lower-case one untested. +if ($pass -and (($childLine -cnotmatch 'FTP_PROXY=\[http://127\.0\.0\.1:8888\]') -or ($childLine -cnotmatch 'ftp_proxy=\[http://127\.0\.0\.1:8888\]'))) { + $pass = $false + $reason = "runner did not inject/scrub the FTP proxy family (expected http://127.0.0.1:8888)" +} + # NO_PROXY / no_proxy must be neutralized to empty so an image-baked or # caller-supplied exemption (e.g. NO_PROXY=*) cannot disable the proxy. if ($pass -and (($childLine -notmatch 'NO_PROXY=\[\]') -or ($childLine -notmatch 'no_proxy=\[\]'))) {