fix(security): prevent path traversal in /web-app/ route#2084
Conversation
fl0rianr
left a comment
There was a problem hiding this comment.
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.
|
Thanks for addressing the Windows separator issue and adding regression tests. This is much closer. I think two things still need tightening before merge:
Small code nit: for (const auto& part : relative) {
if (part == "..") {
res.status = 403;
res.set_content("Forbidden", "text/plain");
return;
}
} |
…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>
75e75b5 to
29866f7
Compare
|
Thanks, this is much improved. The production-side fix looks good to me now: using std::filesystem::relative plus component-wise 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: 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. |
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>
fl0rianr
left a comment
There was a problem hiding this comment.
Thanks! Looks good to me now.
|
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 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:
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. |
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>
15cfb2a to
fc746a4
Compare
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.