Repository navigation
Clear the open CodeQL alerts (#1, #2, #3, #5, #7, #8) - #1009
Conversation
… redirect, flag-named probe refusal - #3/#5/#7 py/path-injection: resolve_static_asset answers by exact name from a listing of the static root (_servable_files: real files whose resolved location stays under the root) instead of joining the request path onto the root. Same containment, stricter on non-canonical spellings, and no request byte reaches a filesystem call. - #2 py/http-response-splitting: the token exchange redirects to a member of shell_request_paths (shell_redirect_target), never to the request's own spelling; also closes a `/\host` protocol-relative redirect. - #1 py/clear-text-logging-sensitive-data: the probe's missing-credential refusal names the missing flag, never the value typed after it. - #8 py/bad-tag-filter: the shell's script-tag test parses with html.parser instead of a regex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
left a comment
There was a problem hiding this comment.
Security review of the CodeQL fixes. No exploitable issue found. I checked that each fix is at least as strict as before: .., absolute, %2e%2e, %252e%252e, null byte, backslash, /A.CSS on case-insensitive APFS, NFC/NFD and non-canonical spellings are all refused now, because it is an exact-name lookup. The redirect Location is always a member of the closed set: CRLF, %5C, \\, //host and /%2Frules all go to /. Every ?token= producer (server.py:170, runtime.py:295) emits /?token=, so no deep link regresses. A per-request walk of 17 files measured about 0.75 ms. The listing serves nothing new: the content-type gate in _serve_static is unchanged. The probe refusal no longer echoes either --*-var value, and the probe has no other print of them. The 4 touched test files pass (399). Two low findings are inline: one docstring and one test that cannot fail.
…t about its premise Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
left a comment
There was a problem hiding this comment.
Round 2 security review. One finding, Low, inline. It is a stale docstring, not a hole.
Both round-1 threads are addressed.
(a) The _servable_files docstring and the module docstring now say that symlinked directories are never descended into, and that is_relative_to covers file symlinks. test_nothing_under_a_symlinked_directory_is_listed_even_inside_the_root pins this, and test_a_symlink_out_of_the_root_is_refused pins the filter.
(b) The raw-CR test now says that it pins the http.server premise and asserts the 400. The encoded-CRLF parametrized cases fail on main, because the old Location echoed /%0d%0a....
What I checked independently:
- Traversal.
.., absolute,%2e%2e,%252e%252e, null byte, backslash and%5c, uppercase on case-insensitive APFS (/JS/main.jswas served before and is a 404 now), trailing slash, and non-canonical spellings. All resolve to nothing, because the request is only compared with keys of the listing. The returnedPathis the listing's own value. That covers_serve_static'sread_bytes(#5). - Redirect.
/\\host,%5C, CRLF,/api/*, an unknown name and%ffall go to/. Every producer of a?token=URL (ServeConfig.url,runtime.url_for) uses/, and/status?token=is still kept. So no deep-link regression. - Nothing newly served. The listing holds the same 17 files.
resolve_client_routeandcontent_type_forare unchanged. - Performance.
resolve_static_assettakes about 0.74 ms per call on the realstatic/. - Tests. The new staticfiles tests are red without the filter or the lookup. The probe test is red on main. The
html.parserrewrite keeps every assertion.test_staticfiles,test_server,test_client_assets,test_robinhood_order_probeandtest_web_prose_stays_truegave 407 passed. - Stale text. I grepped
keel/webandtests/webforrelative_to,resolve()and containment. The only stale text is the one below.
…ic spellings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
left a comment
There was a problem hiding this comment.
Review round 3 (head 8ef4795): no findings.
I re-checked the three resolved threads against the code:
- Symlinked-directory docstrings:
_servable_filesand the module docstring now say thatrglobdoes not descend into symlinked directories, andrequires-python >=3.14makes that the default.test_nothing_under_a_symlinked_directory_is_listed_even_inside_the_rootpins it. - Raw-CR socket test: its docstring now says it pins the stdlib 400 premise and cannot fail against the fix itself. The parametrized percent-encoded cases are what test
shell_redirect_target. _is_a_sessioned_shell_read(server.py:812): the docstring now says the list and_serve_staticagree by construction. That holds:/js/../js/main.jsis neither inshell_request_pathsnor served.
Independent checks:
- Strictness: I probed the resolver against the real
STATIC_ROOT. These all returnNone:.., encoded.., absolute paths,%2f,//, a null byte (%00), a raw backslash,%5C,/JS/main.js(case), and a trailing slash. The listing is a subset of what the old join served, so no file is newly served. It contains no dotfiles. - Redirect:
Locationis always a member ofshell_request_pathsor/.//evil,/\evil,/rules/and/RULESall go to/, and/%72ulesgoes to/rules. Every?token=URL keel builds (ServeConfig.url,runtime.url_for) is rooted at/, so this is not a deep-link regression. - Probe: lines 497-498 are the only uses of
args.*_var, and the refusal echoes neither. - Tests: the non-canonical-spelling case
sub%2fnested.html/would have been served by the old join, so that test is red before the fix. Thehtml.parserassertions check tag structure, not substrings. - Stale prose: no remaining reference to the old join, the old
relative_tomechanism or the old Location behaviour inkeel/web,tests/webordocs/architecture.md. - Test run:
tests/web/test_staticfiles.py,test_server.py,test_client_assets.pyandtests/scripts/test_robinhood_order_probe.py: 400 passed.
Clears the six open CodeQL code-scanning alerts on
main. No alert is dismissed here; the owner decides dismissals.Per alert
keel/web/staticfiles.pyresolve()+relative_to()containment was real; CodeQL does not modelrelative_toas a sanitizer)resolve_static_assetnow answers by exact name from_servable_files(root), a listing of real files whose resolved location stays under the root. The request path is only compared with names the package produced, never joined or walked. At least as strict: also refuses non-canonical spellings (sub/../index.html,./x,a//b, trailing slash) that no browser sends.shell_request_pathsreuses the same listingkeel/web/server.py_serve_staticreadPathit reads is the listing's own value. No code was moved out ofdo_GET/_serve_statictest_server.pykeel/web/server.pyhttp.serversplits the request line on whitespace, andparsed.pathis never percent-decoded, so CR/LF cannot reach the header). Real but minor open redirect:/\evil.example?token=<valid>redirected to/\evil.example, which browsers treat as//evil.example(needs the session token, so low impact)Locationisstaticfiles.shell_redirect_target: the member ofshell_request_pathsthe decoded path names, else/. No request byte can reach the headertest_the_token_exchange_redirects_only_to_a_path_the_console_answers(encoded CRLF,%5Cand raw backslash,/api/pulse, unknown name all go to/;/rulesand/statusare kept), andtest_a_raw_carriage_return_in_the_request_line_injects_no_header(real control bytes over a socket)scripts/robinhood_order_probe.py--private-key-var/--api-key-var, so a key pasted where its variable name belongs went to the terminal.envsuppliedtest_a_missing_credential_is_named_by_its_flag_and_echoes_no_value(red before the fix)tests/web/test_client_assets.pytest_the_shell_ships_no_inline_script_or_stylecollects tags withhtml.parser; same assertions (every script has asrc; the only classic script is exactly{"src": "/js/theme.js"}; it comes before the first stylesheet link; no<style>/<base>)<SCRIPT >, swapping script and stylesheet order, and an extra attribute on the theme script each make it failVerification
ruff check,ruff format --check,mypyare clean.cmp) and restored from a saved copy: dropping the containment filter, droppingis_file, re-adding the join fallback, echoing the request path in the redirect, echoing--private-key-var, and threeindex.htmlmutants. All were killed. A defensivetry/exceptaroundresolve()survived its mutant, so it was dead code (non-strictresolve()does not raise on a loop) and was removed./rules?token=gave a 303 to/rules, the shell rendered "Rules", all 18 resources returned 200, no console errors.🤖 Generated with Claude Code