fix: reserved/private SQL files executable over HTTP after privileged run_sql cache priming#1307
Merged
Merged
Conversation
Reserved/private SQL files (sqlpage/ prefix, dotfiles, .. traversal, absolute paths) became directly routable over HTTP while their parsed form was fresh in sql_file_cache. A trusted page loading such a file via sqlpage.run_sql(...) loads it with privilege and caches it; a later direct unprivileged request hit the fresh cache entry before the path guard ran, returning 200 and executing the private SQL instead of 403. The unprivileged path validation is extracted into filesystem::validate_unprivileged_path and now enforced before consulting the cache in both HTTP routing (AppFileStore::contains) and the unprivileged FileCache::get_with_privilege path.
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.
Private files under the reserved
sqlpage/prefix (and dotfiles,..traversal, absolute paths) became directly executable over HTTP while their parsed form was fresh insql_file_cache.Chain: a trusted page loads such a file via
sqlpage.run_sql(...), which loads it with privilege (functions.rs#L731-L740) and caches the parsed SQL. A later direct unprivileged request then short-circuits the path guard:AppFileStore::containsconsulted the cache before any guard (routing.rs#L132), andFileCache::get_with_privilege(..., false)returns a fresh cache entry beforesafe_local_path(..., false)ever runs (file_cache.rs#L123-L130). SoGET /sqlpage/secret.sql(or the extensionless aliasGET /sqlpage/secret) returned200and executed the private SQL instead of403.Fix: extract the unprivileged guard into
filesystem::validate_unprivileged_path(same logic that was inline insafe_local_path, no behavior change there) and enforce it before trusting the cache, in bothAppFileStore::containsand the unprivilegedFileCacheget path.Proof
cargo test --test mod test_private_path_not_accessible_after_privileged_cache_priming(tests/errors/mod.rs): a trusted page primes the cache viarun_sql('sqlpage/private_cache_bypass_test.sql'), then a direct request for that path (and its extensionless alias) must be403. The test sets a non-zerocache_stale_duration_msso the primed entry stays fresh (default is0in non-prod, which would mask the bug).Before the fix:
After the fix:
Full integration suite (
cargo test --test mod, 64 tests) andcargo clippy --all-targets --all-features -- -D warningspass. The existingtest_privileged_paths_are_not_accessible(uncached path) still passes, confirming no regression on the non-cached case, andrun_sql-based tests still pass, confirming legitimate privileged includes keep working.Reviewer notes
sqlpage_filesvariant is covered too: the routing-level guard runs beforefile_existsconsults the database. The added test uses local files (default test DB has nosqlpage_filestable).403directly, and the cache layer protectsprocess_sql_requesteven if a future caller bypasses routing.