The edge's offline copy added a folder's slash with redir {path}/, so //evil.example/x answered 308 to //evil.example/x/, another site. A path that starts // or /\ (decoded) isn't redirected now; it is asked of the copy as it is and gets its 404. Caddy's path matchers see the path with its slashes merged, so the check reads {path} itself. Scenario: a folder still gets its slash, and //, /%5C and /%2F paths get no Location.- commit
- 835172a22647d43ff8752262a9148c26cf5b2e5e
- parent
- 43f0cccc60bb3c7c724c54b683f5b1a2feb998e8
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 123s, signed by cat
- kind
- fix risk score 1.4 of 3, Jev's estimate
- review
- #30 Security: sign-in, invites, owner pages, lake, caps and the edge
ci attestation
forge-attestation 1 type ci commit 835172a22647d43ff8752262a9148c26cf5b2e5e tree 08efa11caebab6fd0c07daa852bd740b95d3fc1b script .forge/ci 8d2000a416f8160bcf710d98b72af9eb76287f58 result pass exit 0 started 2026-10-19T07:58:09Z duration 123s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:49f18c2403be91531d42a818b6a041c6bfafc781e5843c6c1ec84008ae2f7962 324 lines |pushed wip/i18n |c6c01be running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |c6c01be pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |572cef7 running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |572cef7 fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |82add05 running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |82add05 pass in 0s, signed |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |warning: You appear to have cloned an empty repository. |site home: deployed 62e41ed -> /sites/ana/dotfiles/home/ |created ana/notes |remote: review #1: /ana/notes/reviews/1/ |pushed ana/todo |landing #1 A todo list: ana/todo -> main |19bf5f0 running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |19bf5f0 pass in 0s, signed |pushed main |3593ce6 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |3593ce6 pass in 0s, signed |remote: review #7: /dami/greet/reviews/7/ |pushed dami/regions |created dami/forge |warning: You appear to have cloned an empty repository. |site guide: mkdir -p guide/fonts && cp site/assets/fonts/*.woff2 guide/fonts/ |site guide: deployed 2de227d -> /sites/dami/forge/guide/ |(node:13562) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/dotfiles in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.R1Snr8/wt/demo/root/.forge/www |(node:13741) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/notes in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.R1Snr8/wt/demo/root/.forge/www |(node:13929) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/forge in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.R1Snr8/wt/demo/root/.forge/www |(node:14112) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/greet in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.R1Snr8/wt/demo/root/.forge/www |(node:14829) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built - in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.R1Snr8/wt/demo/root/.forge/www |235 of 235 pages render the same without 11ty |guide: 8 files, links ok |ok
Signature: good, cat
| M | deploy/ | 5 | +++++ |
| A | test/ | 29 | +++++++++++++++++++++++++++++ |
@@ -44,9 +44,14 @@
4444 # Like the forge's file server: /a/b -> /a/b/, /a/b/ -> /a/b/index.html.4545 # Release downloads are files whatever their names (greet-linux,4646 # SHA256SUMS), and download rather than open, as from the forge.47+ # Never a path that starts // or /\ (decoded): redirected, that48+ # would be another site's address (//evil.example/x/). Path49+ # matchers see the path cleaned (one slash), so this looks at50+ # the path itself.4751 @bare {4852 path_regexp ^(.*/)?[^./]+$4953 not path_regexp ^/[^/]+/[^/]+/releases/download/.54+ not vars_regexp {path} ^/[/\\]5055 }5156 redir @bare {path}/ 3085257 @download path_regexp ^/[^/]+/[^/]+/releases/download/.@@ -0,0 +1,29 @@
1+# Offline, the edge adds the slash a folder's address lacks (/a/b -> /a/b/),2+# but never for a path that starts // or /\, where the slash would make3+# Location another site's address (//evil.test/x/): those are asked of the4+# copy as they are, and get its 404.5+. "$(dirname "$0")/../lib.sh"6+command -v caddy >/dev/null 2>&1 || { note "caddy is not installed: skipped running it"; exit 0; }7+mkdir -p "$T/bucket/dami/greet"8+echo '<p>greet</p>' >"$T/bucket/dami/greet/index.html"9+mport=$(free_port)10+printf '{\n\tadmin off\n\tauto_https off\n\tpersist_config off\n\tdefault_bind 127.0.0.1\n}\n\n:%s {\n\troot * %s\n\tfile_server\n}\n' "$mport" "$T/bucket" >"$T/Caddyfile.bucket"11+eport=$(free_port)12+dead=$(free_port)13+# On 127.0.0.1 only, so no other scenario's server can take the port there.14+sed "s/^:8080 {/:$eport {/; s/127\.0\.0\.1:8081/127.0.0.1:$dead/" "$T_HOME/deploy/edge/Caddyfile" |15+ awk '{ print } $0 == "\tadmin off" { print "\tdefault_bind 127.0.0.1" }' >"$T/Caddyfile.edge"16+for c in bucket edge; do17+ FORGE_DOMAIN=src.test FORGE_MIRROR_URL=http://127.0.0.1:$mport XDG_DATA_HOME=$T/xdg XDG_CONFIG_HOME=$T/xdg \18+ caddy run --config "$T/Caddyfile.$c" --adapter caddyfile >"$T/caddy.$c.log" 2>&1 &19+ T_PIDS="$T_PIDS $!"20+done21+wait_http "http://127.0.0.1:$mport/"22+wait_http "http://127.0.0.1:$eport/"23+e=http://127.0.0.1:$eport24+eq "$(http -s -o /dev/null -w '%{http_code} %{redirect_url}' -H 'Host: src.test' "$e/dami/greet")" "308 $e/dami/greet/"25+for p in //evil.test/x //evil.test /%5Cevil.test/x /%2Fevil.test/x; do26+ http -s -i -H 'Host: src.test' "$e$p" | tr -d '\r' >"$T/res"27+ lacks "$T/res" '^Location:'28+ has "$T/res" '^HTTP/1.1 404 '29+done