SECURITY: WebDAV GET serves unindexed .html/.svg inline with their active type #983

Open
opened 2026-10-03 06:47:44 +00:00 by kayg · 5 comments
Owner

Found (review of job/noext-851, 2026-10-03); older gap, not caused by #851

When a file has no indexed MIME type, WebDAV GET serves dav-server's guess from the file name, inline: x.html → text/html, x.svg → image/svg+xml, with no Content-Disposition. nosniff and the API CSP default-src 'none' block scripts (crates/calternal-server/src/security.rs), but that CSP has no sandbox, so an HTML form or phishing page can render on the WebDAV origin path.

Fix

Apply is_active_mime (calternal-media) to the final Content-Type on every WebDAV GET/HEAD response regardless of index state (active → text/plain), and add sandbox to the WebDAV response CSP. Adversarial cases: upload x.html / x.svg / x.xhtml via WebDAV, GET before indexing finishes, expect a non-active type or sandbox CSP.

## Found (review of job/noext-851, 2026-10-03); older gap, not caused by #851 When a file has no indexed MIME type, WebDAV GET serves dav-server's guess from the file name, inline: `x.html` → `text/html`, `x.svg` → `image/svg+xml`, with no `Content-Disposition`. `nosniff` and the API CSP `default-src 'none'` block scripts (`crates/calternal-server/src/security.rs`), but that CSP has no `sandbox`, so an HTML form or phishing page can render on the WebDAV origin path. ## Fix Apply `is_active_mime` (calternal-media) to the final `Content-Type` on every WebDAV GET/HEAD response regardless of index state (active → `text/plain`), and add `sandbox` to the WebDAV response CSP. Adversarial cases: upload `x.html` / `x.svg` / `x.xhtml` via WebDAV, GET before indexing finishes, expect a non-active type or sandbox CSP.
Author
Owner

Starting #983 in job/davactive-983. Base SHA: 48c94c9776660cee105be86c5a6ace90dd425367; current checkpoint HEAD: 3a7ab14bcc99f12b5f0947beabb0a0b424b82ad6. I am reviewing the checkpoint against the issue, then I will reuse the shared MIME check and add the requested DAV regressions.

Starting #983 in `job/davactive-983`. Base SHA: `48c94c9776660cee105be86c5a6ace90dd425367`; current checkpoint HEAD: `3a7ab14bcc99f12b5f0947beabb0a0b424b82ad6`. I am reviewing the checkpoint against the issue, then I will reuse the shared MIME check and add the requested DAV regressions.
Author
Owner

Checkpoint review finding: it introduced calternal-dav::active_type, while this checkout (including fetched origin/dev) has no calternal-media crate or is_active_mime; the existing rule is private in crates/plugins/files/src/user_bytes.rs. I will add the small shared calternal-media::is_active_mime API named by the issue, then use it from Files and the WebDAV final-header filter. This keeps MIME policy in one place.

Checkpoint review finding: it introduced `calternal-dav::active_type`, while this checkout (including fetched `origin/dev`) has no `calternal-media` crate or `is_active_mime`; the existing rule is private in `crates/plugins/files/src/user_bytes.rs`. I will add the small shared `calternal-media::is_active_mime` API named by the issue, then use it from Files and the WebDAV final-header filter. This keeps MIME policy in one place.
Author
Owner

Verification finding: the first cargo test -p calternal-plugin-files run completed 145 tests successfully, with 1 ignored and tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm failing. It exceeded the test’s 300-second timeout; the writer also reported atomic write 703 failed: entry not found at unchanged crates/plugins/files/src/lib.rs:5086. The MIME tests passed. I did not change this existing expectation or retry the full suite; it needs review in the merge round.

Verification finding: the first `cargo test -p calternal-plugin-files` run completed 145 tests successfully, with 1 ignored and `tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm` failing. It exceeded the test’s 300-second timeout; the writer also reported `atomic write 703 failed: entry not found` at unchanged `crates/plugins/files/src/lib.rs:5086`. The MIME tests passed. I did not change this existing expectation or retry the full suite; it needs review in the merge round.
Author
Owner

Verification finding: cargo test -p calternal-server completed 107 tests successfully, with 3 ignored and wire::tests::live_apps_run_in_separate_processes failing. Its child wire::tests::full_app_setup_session_config_and_backup hit the existing 45-second timeout (Elapsed(()) at unchanged crates/calternal-server/src/wire.rs:7679). All security::tests::* passed, including the DAV policy and route-specific CSP checks. I did not change the existing expectation or retry the full suite; include the timeout in the merge round.

Verification finding: `cargo test -p calternal-server` completed 107 tests successfully, with 3 ignored and `wire::tests::live_apps_run_in_separate_processes` failing. Its child `wire::tests::full_app_setup_session_config_and_backup` hit the existing 45-second timeout (`Elapsed(())` at unchanged `crates/calternal-server/src/wire.rs:7679`). All `security::tests::*` passed, including the DAV policy and route-specific CSP checks. I did not change the existing expectation or retry the full suite; include the timeout in the merge round.
Author
Owner

#983 complete

Implemented the active MIME check on WebDAV GET and HEAD responses. Active file types now go out as text/plain; charset=utf-8, including unindexed .html, .svg and .xhtml files. The response bytes stay unchanged. DAV responses also get an enforced CSP with sandbox, including routes that already set their own CSP. The WebDAV probe checks both the final type and every enforced CSP policy.

The fetched origin/dev did not contain a calternal-media crate or is_active_mime. I added the small shared crate and moved the Files plugin's existing private MIME rule into it. The Files plugin and WebDAV now use the same check. The WIP checkpoint remains in branch history; these changes are in focused commits:

  • 4f58bcc5f — centralize MIME safety rules for file responses
  • fd0497956 — neutralize active MIME responses and sandbox DAV pages
  • 1df980db5 — document probe handling for repeated CSP policies

Head SHA: 1df980db59f45b49c5a1afed06775cd2d328992b

Files changed: Cargo.lock; crates/calternal-media/Cargo.toml; crates/calternal-media/src/lib.rs; crates/plugins/files/Cargo.toml; crates/plugins/files/src/user_bytes.rs; crates/calternal-dav/Cargo.toml; crates/calternal-dav/src/active_type.rs (removed); crates/calternal-dav/src/files.rs; crates/calternal-dav/src/lib.rs; crates/calternal-dav/tests/active_types.rs; crates/calternal-server/src/security.rs; tests/adversarial/test_dav_probe.py; tests/adversarial/webdav.py.

Gates

cargo fmt --check completed with exit status 0 and no output.

cargo clippy -p calternal-media --all-targets -- -D warnings:

    Checking calternal-media v0.0.1 (/home/kayg/Developer/calternal-wt/davactive-983/crates/calternal-media)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.12s

cargo test -p calternal-media:

running 2 tests
test tests::recognizes_active_mime_values ... ok
test tests::leaves_passive_mime_values_inactive ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

cargo clippy -p calternal-plugin-files --all-targets -- -D warnings completed successfully:

    Checking calternal-plugin-files v0.0.1 (/home/kayg/Developer/calternal-wt/davactive-983/crates/plugins/files)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 10m 16s

cargo test -p calternal-plugin-files had one existing stress-test failure under host load:

thread 'tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm' panicked at crates/plugins/files/src/lib.rs:5112:14:
writes, reconcile scans, and watcher adoption complete within five minutes: Elapsed(())

thread 'tokio-rt-worker' panicked at crates/plugins/files/src/lib.rs:5086:45:
atomic write 703 failed: entry not found

test result: FAILED. 145 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 357.55s

error: test failed, to rerun pass `-p calternal-plugin-files --lib`

The test and its expectation are unchanged. All user_bytes::tests::* passed in this run.

cargo clippy -p calternal-dav --all-targets -- -D warnings completed successfully:

    Checking calternal-dav v0.0.1 (/home/kayg/Developer/calternal-wt/davactive-983/crates/calternal-dav)
    Checking proptest v1.11.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 02s

cargo test -p calternal-dav:

test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s

cargo clippy -p calternal-server --all-targets -- -D warnings completed successfully:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 09s

cargo test -p calternal-server had one existing live-app timeout:

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 110 filtered out; finished in 45.45s

test result: FAILED. 107 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out; finished in 84.26s

error: test failed, to rerun pass `-p calternal-server --bin calternal-server`

The child test wire::tests::full_app_setup_session_config_and_backup hit its existing 45-second bound (Elapsed(()) at unchanged crates/calternal-server/src/wire.rs:7679). All security::tests::* passed.

Focused probe tests:

....
----------------------------------------------------------------------
Ran 4 tests in 0.860s

OK

Gaps and merge round

I did not run the real-server WebDAV probe or the broader adversarial matrices in this job. The merge round should run python3 tests/adversarial/webdav.py against its local server with the required CALTERNAL_WEBDAV_* variables set. It must prove that immediate GET and HEAD after each .html, .svg and .xhtml upload have a non-active type, a sandbox policy and unchanged GET bytes.

The merge round should also run cargo test -p calternal-plugin-files and cargo test -p calternal-server to check whether the two existing timeouts reproduce under its test conditions. I did not change either expectation or retry either full suite.

No UI changed. No performance measurement was run because this is a security hardening issue; the merge-round verification policy reserves performance measurements for performance issues. The only design choice not already present in DESIGN was adding calternal-media because the named shared crate and helper were absent from fetched origin/dev.

## #983 complete Implemented the active MIME check on WebDAV GET and HEAD responses. Active file types now go out as `text/plain; charset=utf-8`, including unindexed `.html`, `.svg` and `.xhtml` files. The response bytes stay unchanged. DAV responses also get an enforced CSP with `sandbox`, including routes that already set their own CSP. The WebDAV probe checks both the final type and every enforced CSP policy. The fetched `origin/dev` did not contain a `calternal-media` crate or `is_active_mime`. I added the small shared crate and moved the Files plugin's existing private MIME rule into it. The Files plugin and WebDAV now use the same check. The WIP checkpoint remains in branch history; these changes are in focused commits: - `4f58bcc5f` — centralize MIME safety rules for file responses - `fd0497956` — neutralize active MIME responses and sandbox DAV pages - `1df980db5` — document probe handling for repeated CSP policies Head SHA: `1df980db59f45b49c5a1afed06775cd2d328992b` Files changed: `Cargo.lock`; `crates/calternal-media/Cargo.toml`; `crates/calternal-media/src/lib.rs`; `crates/plugins/files/Cargo.toml`; `crates/plugins/files/src/user_bytes.rs`; `crates/calternal-dav/Cargo.toml`; `crates/calternal-dav/src/active_type.rs` (removed); `crates/calternal-dav/src/files.rs`; `crates/calternal-dav/src/lib.rs`; `crates/calternal-dav/tests/active_types.rs`; `crates/calternal-server/src/security.rs`; `tests/adversarial/test_dav_probe.py`; `tests/adversarial/webdav.py`. ## Gates `cargo fmt --check` completed with exit status 0 and no output. `cargo clippy -p calternal-media --all-targets -- -D warnings`: ```text Checking calternal-media v0.0.1 (/home/kayg/Developer/calternal-wt/davactive-983/crates/calternal-media) Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.12s ``` `cargo test -p calternal-media`: ```text running 2 tests test tests::recognizes_active_mime_values ... ok test tests::leaves_passive_mime_values_inactive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s ``` `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings` completed successfully: ```text Checking calternal-plugin-files v0.0.1 (/home/kayg/Developer/calternal-wt/davactive-983/crates/plugins/files) Finished `dev` profile [unoptimized + debuginfo] target(s) in 10m 16s ``` `cargo test -p calternal-plugin-files` had one existing stress-test failure under host load: ```text thread 'tests::internal_temp_paths_never_enter_index_during_atomic_write_reconcile_storm' panicked at crates/plugins/files/src/lib.rs:5112:14: writes, reconcile scans, and watcher adoption complete within five minutes: Elapsed(()) thread 'tokio-rt-worker' panicked at crates/plugins/files/src/lib.rs:5086:45: atomic write 703 failed: entry not found test result: FAILED. 145 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 357.55s error: test failed, to rerun pass `-p calternal-plugin-files --lib` ``` The test and its expectation are unchanged. All `user_bytes::tests::*` passed in this run. `cargo clippy -p calternal-dav --all-targets -- -D warnings` completed successfully: ```text Checking calternal-dav v0.0.1 (/home/kayg/Developer/calternal-wt/davactive-983/crates/calternal-dav) Checking proptest v1.11.0 Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 02s ``` `cargo test -p calternal-dav`: ```text test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.12s test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s ``` `cargo clippy -p calternal-server --all-targets -- -D warnings` completed successfully: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 6m 09s ``` `cargo test -p calternal-server` had one existing live-app timeout: ```text test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 110 filtered out; finished in 45.45s test result: FAILED. 107 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out; finished in 84.26s error: test failed, to rerun pass `-p calternal-server --bin calternal-server` ``` The child test `wire::tests::full_app_setup_session_config_and_backup` hit its existing 45-second bound (`Elapsed(())` at unchanged `crates/calternal-server/src/wire.rs:7679`). All `security::tests::*` passed. Focused probe tests: ```text .... ---------------------------------------------------------------------- Ran 4 tests in 0.860s OK ``` ## Gaps and merge round I did not run the real-server WebDAV probe or the broader adversarial matrices in this job. The merge round should run `python3 tests/adversarial/webdav.py` against its local server with the required `CALTERNAL_WEBDAV_*` variables set. It must prove that immediate GET and HEAD after each `.html`, `.svg` and `.xhtml` upload have a non-active type, a sandbox policy and unchanged GET bytes. The merge round should also run `cargo test -p calternal-plugin-files` and `cargo test -p calternal-server` to check whether the two existing timeouts reproduce under its test conditions. I did not change either expectation or retry either full suite. No UI changed. No performance measurement was run because this is a security hardening issue; the merge-round verification policy reserves performance measurements for performance issues. The only design choice not already present in DESIGN was adding `calternal-media` because the named shared crate and helper were absent from fetched `origin/dev`.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#983
No description provided.