* refactor: split SQLPage functions into one module each
Each built-in `sqlpage.*` function is now a plain `async fn` in its own
file under `sqlpage_functions/functions/`, with an ordinary Rust signature
and no marker comments or macros inside it.
Registration is automatic: `build.rs` lists the files in `functions/` into a
`sqlpage_functions!` call (the only generated code, one line per function),
and that macro declares the modules and builds the `SqlPageFunctionName`
enum the SQL engine dispatches on. There is no marker-comment parsing and no
per-argument codegen. Argument extraction, dispatch and return-value
conversion are ordinary generic code in function_traits.rs (`Extract`,
`Handler`, `IntoCowResult`), using the same `Fn`-arity trick axum uses for
handlers, so a function's argument and return types are read straight from
its signature.
Also folds in main's FileAccess centralization (#1327) for the file-reading
functions.
Verified: `cargo test` passes (148 lib + 70 integration tests, including
run_all_sql_test_files, the hmac webhook tests, the upload tests and
test_file_upload_through_runsql which exercises the &mut DbConn path).
`cargo clippy` and `cargo fmt --check` are clean.
* less magic
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been cancelled
The relative-redirect check accepted /\evil.test (and /<TAB>/evil.test):
the WHATWG URL parser used by the url crate treats \ as / and strips ASCII
tab/newline/CR, so url::Url::join turned these into the external authority
evil.test when building post_logout_redirect_uri. is_safe_relative_redirect now
rejects any backslash or ASCII control character, applied via one shared helper
in verify_logout_params, validate_redirect_url, and sqlpage.oidc_logout_url.
* fix(oidc): bind logout URLs to the current session (forced-logout CSRF)
Logout URLs were signed only over the redirect target and a timestamp, so any
valid, unexpired logout URL would clear whoever's cookies followed it. The
logout signature now also covers the caller's sqlpage_auth cookie, so a logout
URL only logs out the session it was issued for. Generation and verification
select the same cookie (the last of any duplicates, matching how RequestInfo
merges them) so the check stays consistent.
* style: run cargo fmt on the logout cookie test
In production, error responses must not leak the SQL statement, the source
file path, the raw database error, environment values, or configuration, for
ANY output format. In development the full detail is shown, and the full error
is always logged server-side.
This centralizes the dev-vs-production decision in a single place. An internal
error stays a full `anyhow::Error` everywhere; the only place that turns it into
a user-facing representation is `ClientError::new` in `src/webserver/error.rs`,
which is also the only caller of `DevOrProd::is_prod`. Every renderer (HTML,
JSON, NDJSON, SSE, CSV) and the header/pre-body path obtains a `ClientError`
from that one function and only formats it; none of them inspect the
environment. Leaking is therefore impossible by construction: a renderer cannot
emit what it never receives.
The error type, the production message, `get_backtrace_as_strings`, and the
error-component data construction live in `error.rs`; `render.rs` only renders
components and formats a `ClientError`.
An unauthenticated request could encode a byte of a protected prefix (e.g.
/%70rotected/, which the router decodes to /protected/ and serves) to bypass
oidc_protected_paths. is_public_path now percent-decodes both the request path
and the configured prefixes before the plain string-prefix comparison, so
encoded and plain URLs are classified identically, including when site_prefix
contains percent-encoded characters such as a space. Plain string matching
preserves the documented /public vs /public/ distinction.
Attacks that require injecting attacker-chosen cookies into the victim's
browser (e.g. OIDC login CSRF / session fixation via a forged login-flow
-state cookie) are out of scope: SQLPage assumes its origin cookie jar is
writable only by the user agent.
The csv and download components built the Content-Disposition header by
string-interpolating the user-supplied filename. A filename containing
characters such as ';', '"' or '=' could inject an additional header
parameter (e.g. a second, agent-preferred filename*=...), letting an app
that interpolates untrusted data into the filename smuggle a different
download name past the intended one.
Build the header with actix-web's structured ContentDisposition type so
the filename is always a single, properly quoted/escaped value and cannot
create new parameters.
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.
* docs: warn that static serving follows symlinks under web_root
Operators control web_root contents, so a symlink there is a trusted
deployment artifact. Clarify that SQLPage follows such symlinks during
static file serving, meaning a symlink under web_root pointing to
reserved/private files (sqlpage/ config, dotfiles) or to files outside
web_root would make those targets publicly reachable.
Note added to SECURITY.md (Out of Scope), cross-referenced from the
web_root row in configuration.md and an Unreleased CHANGELOG entry.
* Update web_root description for clarity
Document that the folder/destination_folder argument of
sqlpage.persist_uploaded_file must be chosen by the app author and never
derived from untrusted request data. It is joined directly to the web
root, so a value containing '..' or an absolute path would write the
uploaded file outside the web root. Docs-only clarification of existing
intended behavior; no logic change.
* Improve CI Docker cache reuse
* Ignore git metadata in Docker builds
* Use BuildKit cache mounts for Cargo Docker builds
* Make Docker dependency layer match real build inputs
* Revert "Make Docker dependency layer match real build inputs"
This reverts commit 3a9372150ed30437c0b7872907a3e03ce1c38f5d.
* Revert "Use BuildKit cache mounts for Cargo Docker builds"
This reverts commit dac2c5264bee72fc287877fcfe15e49c319d99dd.
* Test CI incremental Rust caches
* Remove slow Windows port diagnostic
* Narrow Windows incremental cache
* Reuse Linux test artifacts in CI
* Avoid duplicate Linux test execution in CI
* Isolate Docker cache scopes by recipe
* Warm Docker dependencies for lib target
* Warm Docker dependencies with Cargo config
* Stabilize Docker metadata for cache keys
* Use Git context for Docker builds