Repository navigation
fix: restrict server file access to known logs and plain names (#374) - #376
Merged
Merged
Conversation
/log read any file the server process could access, and the file it read set latest_log_dir, which let the unauthenticated catch-all route serve every file in that folder. The not-found page also echoed the path unescaped. Several mutation endpoints had the same root cause: file names from the request were joined to a folder unchecked, so /add-log, /add-log-file, /remove-log, /add-output-file and the output_alias of /add-outputs could write or delete files outside their folder. These endpoints are open by default because auth is off unless credentials are configured. - /log only serves files in the log folder, or the log next to an output stored in the database. Anything else gets a 404 page with the path HTML-escaped. - The catch-all checks containment with relative_to instead of a string prefix, and only serves file types a log embeds (images, video, HTML). - Request file names must be plain names (server_paths.safe_file_name). - Uploaded outputs are processed from a TemporaryDirectory, so an upload can no longer overwrite or delete a file in the working directory. - Run deletes and the path update use bound parameters. The old .format() constants are kept for custom database classes that import them, and example/database/sqlite3.py mirrors the change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #374
Problem
/log?path=served any file the server process could read. While investigating, I found the same root cause in several other server endpoints: a path or file name from the request was used without restricting it to the server's own folders. Some of these endpoints write or delete files. They are open by default, because auth is only enabled when credentials are configured.Root cause
/logresolved and readpathwith no allow-list. It also setlatest_log_dirto that file's folder, so the catch-all route then served everything in it. The catch-all checked containment with a string prefix (/srv/logsalso matched/srv/logs-secret). The not-found page echoedpathwithout HTML escaping./add-log,/add-log-file,/remove-log,/add-output-fileandoutput_aliasin/add-outputsjoined request file names onto a folder (or the working directory) without checking them..format().Fix
/logonly serves files insiderobot_logs/, or the log next to an output stored in the database (output-XYZ.xml↔log-XYZ.html, the same mapping the dashboard uses). Anything else returns a real 404 with the path HTML-escaped, including malformed paths.relative_to. It only serves file types a log embeds or links to (images, video, HTML such as the report), so a database or secret file next to a log is not served.server_paths.safe_file_name). Names with a folder part are rejected with an error response. The listener and admin page already send plain names./add-output-file,/add-outputswithoutput_data) are processed from aTemporaryDirectoryinstead of the working directory, so an upload can't overwrite or delete an existing file there.*_BY_RUN_STARTconstants). The old.format()constants stay, marked deprecated, so custom database classes that import them keep working.example/database/sqlite3.pymirrors the change.dashboard-server.md,custom-database-class.mdand theserver-apiskill describe the new restrictions.Behaviour changes to note
update_output_pathreplaces the path as before.--databaseclassthat doesn't implement the optional_get_run_paths,/logonly serves logs fromrobot_logs/./logreturns status 404 instead of 200 for unknown logs.Tests
tests/python/test_server.pyandtest_database.py: read, relative and absolute traversal on/log, sibling-prefix and file-type checks on the catch-all, escaping, malformed paths, rejected names on every write/remove endpoint, the working directory staying untouched, and a quote in a stored path.main(12 failed before the follow-up review fixes were added) and pass on this branch.434 passed. Robot (Docker):132 tests, 132 passed, 0 failed.--serverfrom source: the issue'scurlnow returns 404, and upload/remove with../is rejected while the target file stays intact.Out of scope
output_path/output_folder_pathin/add-outputsread any server path by design.🤖 Generated with Claude Code