Skip to content

fix: reserved/private SQL files executable over HTTP after privileged run_sql cache priming#1307

Merged
lovasoa merged 1 commit into
mainfrom
ophir.lojkine/fix-cache-privilege-bypass
Jun 10, 2026
Merged

fix: reserved/private SQL files executable over HTTP after privileged run_sql cache priming#1307
lovasoa merged 1 commit into
mainfrom
ophir.lojkine/fix-cache-privilege-bypass

Conversation

@lovasoa

@lovasoa lovasoa commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Private files under the reserved sqlpage/ prefix (and dotfiles, .. traversal, absolute paths) became directly executable over HTTP while their parsed form was fresh in sql_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::contains consulted the cache before any guard (routing.rs#L132), and FileCache::get_with_privilege(..., false) returns a fresh cache entry before safe_local_path(..., false) ever runs (file_cache.rs#L123-L130). So GET /sqlpage/secret.sql (or the extensionless alias GET /sqlpage/secret) returned 200 and executed the private SQL instead of 403.

Fix: extract the unprivileged guard into filesystem::validate_unprivileged_path (same logic that was inline in safe_local_path, no behavior change there) and enforce it before trusting the cache, in both AppFileStore::contains and the unprivileged FileCache get 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 via run_sql('sqlpage/private_cache_bypass_test.sql'), then a direct request for that path (and its extensionless alias) must be 403. The test sets a non-zero cache_stale_duration_ms so the primed entry stays fresh (default is 0 in non-prod, which would mask the bug).

Before the fix:

assertion `left == right` failed: /sqlpage/private_cache_bypass_test.sql must be forbidden even after privileged cache priming, got 200 OK
  left: 200
 right: 403

After the fix:

test errors::test_private_path_not_accessible_after_privileged_cache_priming ... ok

Full integration suite (cargo test --test mod, 64 tests) and cargo clippy --all-targets --all-features -- -D warnings pass. The existing test_privileged_paths_are_not_accessible (uncached path) still passes, confirming no regression on the non-cached case, and run_sql-based tests still pass, confirming legitimate privileged includes keep working.

Reviewer notes

  • The DB-backed sqlpage_files variant is covered too: the routing-level guard runs before file_exists consults the database. The added test uses local files (default test DB has no sqlpage_files table).
  • The guard is intentionally enforced in two places (routing and the file cache) for defense in depth: routing yields 403 directly, and the cache layer protects process_sql_request even if a future caller bypasses routing.

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.
@lovasoa lovasoa merged commit d92a739 into main Jun 10, 2026
51 checks passed
@lovasoa lovasoa deleted the ophir.lojkine/fix-cache-privilege-bypass branch June 10, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant