Skip to content

fix(security): prevent path traversal in /web-app/ route#2084

Open
superm1 wants to merge 3 commits into
mainfrom
superm1/SWSPLAT-24174
Open

fix(security): prevent path traversal in /web-app/ route#2084
superm1 wants to merge 3 commits into
mainfrom
superm1/SWSPLAT-24174

Conversation

@superm1
Copy link
Copy Markdown
Member

@superm1 superm1 commented Jun 2, 2026

The /web-app/(.+) route concatenated the regex capture directly onto web_app_dir with no canonicalization or .. filtering. cpp-httplib URL-decodes %2f → / and %2e → . before regex matching, allowing GET /web-app/..%2f..%2f..%2fetc%2fpasswd to read arbitrary files.

Add path canonicalization via std::filesystem::weakly_canonical and verify the resolved path is confined under web_app_dir before opening. Reject any path that escapes the base directory with 403 Forbidden.

Copy link
Copy Markdown
Collaborator

@fl0rianr fl0rianr left a comment

Choose a reason for hiding this comment

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

Thanks for fixing the traversal issue. I think the approach is directionally right, but I would not merge this as-is yet.

The confinement check currently compares candidate.string() against base.string() + "/". That is likely broken on Windows because std::filesystem::path::string() uses native separators, so a valid candidate like C:\...\web-app\renderer.bundle.js will not start with C:\...\web-app/ and may be rejected as 403.

Could we avoid string-prefix checks here and compare path components or use std::filesystem::relative(candidate, base) instead?

Also, since this is a security fix, would you consider adding a regression tests for:

  • a normal /web-app/... asset still returning 200,
  • encoded traversal such as /web-app/..%2f..%2f... not leaking files,
  • nonexistent assets returning 404,
  • ideally Windows coverage or at least a Windows validation note.

@fl0rianr
Copy link
Copy Markdown
Collaborator

fl0rianr commented Jun 3, 2026

Thanks for addressing the Windows separator issue and adding regression tests. This is much closer.

I think two things still need tightening before merge:

  1. The new test/server_webapp.py does not appear to be wired into CI. Since this is a security fix, could we add it to the relevant Linux/Windows hosted test jobs?

  2. The traversal assertions are currently too weak because they only assert status_code != 200. A vulnerable implementation could still pass if the attempted target does not exist or the path is normalized differently before reaching the server. Could we make the test use a known existing file outside web-app (for example a sibling resource file) and assert 403 specifically?

Small code nit: rel_str.find("..") will also reject legitimate asset names containing ... It would probably be safer to check path components instead:

for (const auto& part : relative) {
    if (part == "..") {
        res.status = 403;
        res.set_content("Forbidden", "text/plain");
        return;
    }
}

@github-actions github-actions Bot added area::api HTTP REST API surface and route handlers bug Something isn't working labels Jun 6, 2026
superm1 added a commit that referenced this pull request Jun 7, 2026
…ck and CI

Address review feedback from fl0rianr on PR #2084:

1. Replace substring-based ".." check with component iteration
   - Previous rel_str.find("..") would reject legitimate filenames like "my..file.js"
   - Now iterates path components and only rejects if component == ".."
   - This allows legitimate filenames while still catching traversal attempts

2. Strengthen tests to assert 403 specifically (not just "not 200")
   - test_004 now uses backend_versions.json as a known existing file
   - Explicitly asserts 403 Forbidden, not 404 or other status codes
   - This proves the traversal check is working, not just the file-not-found path

3. Add test for legitimate filenames with ".." substring
   - test_008 verifies "my..test..file.js" returns 404 (not found), not 403 (forbidden)
   - Confirms we're checking components, not substrings

4. Wire server_webapp.py into CI
   - Added to macOS unsigned build tests
   - Added to Linux hosted test matrix
   - Added to Windows/macOS hosted test matrix
   - Runs as separate "webapp" test_type in cpp_server_build_test_release.yml

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
@superm1 superm1 force-pushed the superm1/SWSPLAT-24174 branch from 75e75b5 to 29866f7 Compare June 7, 2026 12:52
@fl0rianr
Copy link
Copy Markdown
Collaborator

fl0rianr commented Jun 7, 2026

Thanks, this is much improved. The production-side fix looks good to me now: using std::filesystem::relative plus component-wise .. checking addresses the Windows separator issue and avoids rejecting legitimate filenames containing ..

The main traversal regression test is also much stronger now because it targets a known existing file outside web-app and asserts 403.

One remaining CI coverage issue before I’d approve: test/server_webapp.py appears to be wired only into the macOS .dmg build’s unsigned path:

if: steps.check_signing.outputs.has_signing != 'true'

In the current PR run the signed .pkg path is active, so that unsigned-path block is skipped. The regular Linux/Windows CLI/Endpoints jobs also don’t appear to run server_webapp.py.

Would you consider to add test/server_webapp.py to a PR job that always runs, ideally the hosted Linux and/or Windows CLI/Endpoints job?

Finally a minor nit - non-blocking: test_002_normal_css_asset_returns_200 actually tests a hashed SVG asset, so renaming it to SVG or using a more stable asset would be cleaner.

superm1 added a commit that referenced this pull request Jun 7, 2026
Address final review feedback from fl0rianr on PR #2084:

1. Add server_webapp.py to Linux hosted job (test-cli-endpoints-linux)
   - This job always runs on ubuntu-latest
   - Previously the test was only in the macOS unsigned path which skips when signed

2. Add server_webapp.py to Windows/macOS hosted job (test-cli-endpoints-windows-macos)
   - Runs on both windows-latest and macos-latest
   - Uses same conditional as other tests to skip on unsigned macOS builds

3. Rename test_002_normal_css_asset_returns_200 to test_002_normal_svg_asset_returns_200
   - The test actually checks an SVG asset, not CSS
   - Function name now matches what it tests

These changes ensure the path traversal regression tests run on every PR
in jobs that always execute (not conditional on signing).

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@fl0rianr fl0rianr left a comment

Choose a reason for hiding this comment

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

Thanks! Looks good to me now.

@fl0rianr
Copy link
Copy Markdown
Collaborator

fl0rianr commented Jun 7, 2026

CI found a real issue with the new regression test. The production-side path traversal fix still looks fine to me, but test_002_normal_svg_asset_returns_200 is too brittle: it hardcodes ecd3a2194d527cfbf0bc.svg, and that file is not present in the Windows package/run, so the test fails with 404.

I’d suggest removing this SVG-specific test or changing it to discover an asset from index.html dynamically. For the security regression, we already have the important coverage:

  • renderer.bundle.js verifies normal asset serving,
  • /web-app/renderer.bundle.js verifies the prefixed route,
  • the encoded traversal test verifies a known existing file outside web-app returns 403.

So I don’t think we need a hardcoded hashed SVG asset here. Once that brittle test is removed or made dynamic, this should be good again.

superm1 and others added 3 commits June 7, 2026 19:13
The /web-app/(.+) route concatenated the regex capture directly onto
web_app_dir with no canonicalization or .. filtering. cpp-httplib
URL-decodes %2f → / and %2e → . before regex matching, allowing
GET /web-app/..%2f..%2f..%2fetc%2fpasswd to read arbitrary files.

Add path canonicalization via std::filesystem::weakly_canonical and
verify the resolved path is confined under web_app_dir before opening.
Reject any path that escapes the base directory with 403 Forbidden.
Address final review feedback from fl0rianr on PR #2084:

1. Add server_webapp.py to Linux hosted job (test-cli-endpoints-linux)
   - This job always runs on ubuntu-latest
   - Previously the test was only in the macOS unsigned path which skips when signed

2. Add server_webapp.py to Windows/macOS hosted job (test-cli-endpoints-windows-macos)
   - Runs on both windows-latest and macos-latest
   - Uses same conditional as other tests to skip on unsigned macOS builds

3. Rename test_002_normal_css_asset_returns_200 to test_002_normal_svg_asset_returns_200
   - The test actually checks an SVG asset, not CSS
   - Function name now matches what it tests

These changes ensure the path traversal regression tests run on every PR
in jobs that always execute (not conditional on signing).

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
@superm1 superm1 force-pushed the superm1/SWSPLAT-24174 branch from 15cfb2a to fc746a4 Compare June 8, 2026 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area::api HTTP REST API surface and route handlers bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants