Files
sqlpage--sqlpage/tests/errors/mod.rs
T
wehub-resource-sync d718c5a372
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:57 +08:00

176 lines
6.3 KiB
Rust

use actix_web::{
http::{self, StatusCode},
test,
};
use sqlpage::{AppState, webserver::http::main_handler};
use crate::common::{make_app_data_from_config, req_path, req_path_with_app_data, test_config};
mod basic_auth;
mod invalid_header;
/// Sends a direct unprivileged GET request through the main handler and returns the
/// resulting HTTP status, whether the handler returned an `Ok` response or an `Err`.
async fn direct_request_status(path: &str, app_data: actix_web::web::Data<AppState>) -> StatusCode {
let req = test::TestRequest::get()
.uri(path)
.app_data(app_data)
.insert_header(actix_web::http::header::Accept::html())
.to_srv_request();
match main_handler(req).await {
Ok(resp) => resp.status(),
Err(e) => e.error_response().status(),
}
}
/// Regression test for a cache privilege-escalation bug.
///
/// `sqlpage.run_sql(...)` loads include files with privilege, so it is allowed to
/// load reserved files under the `sqlpage/` prefix and stores their parsed form in
/// the shared `sql_file_cache`. A subsequent *direct* unprivileged HTTP request for
/// that same reserved path must still be rejected with 403, even while the cache
/// entry is fresh. Before the fix, the fresh cache hit short-circuited the
/// unprivileged path guard and the private SQL was executed and served.
#[actix_web::test]
async fn test_private_path_not_accessible_after_privileged_cache_priming() {
// Keep cache entries "fresh" so the bug (skipping the path guard on fresh hits) is exercised.
let mut config = test_config();
config.cache_stale_duration_ms = Some(60_000);
let app_data = make_app_data_from_config(config).await;
// 1. A trusted page primes the cache by loading the reserved file with privilege.
let prime = req_path_with_app_data("/tests/errors/prime_private_cache.sql", app_data.clone())
.await
.expect("priming page should render");
assert_eq!(prime.status(), StatusCode::OK);
let prime_body = String::from_utf8(test::read_body(prime).await.to_vec()).unwrap();
assert!(
prime_body.contains("private cache bypass secret"),
"priming page should have executed the private file via run_sql, got: {prime_body}"
);
// 2. A direct unprivileged HTTP request for the reserved path must stay forbidden.
for path in [
"/sqlpage/private_cache_bypass_test.sql",
// Extensionless alias: routing appends .sql and finds the fresh cache entry.
"/sqlpage/private_cache_bypass_test",
] {
let status = direct_request_status(path, app_data.clone()).await;
assert_eq!(
status,
StatusCode::FORBIDDEN,
"{path} must be forbidden even after privileged cache priming, got {status}"
);
}
}
#[actix_web::test]
async fn test_privileged_paths_are_not_accessible() {
let resp_result = req_path("/sqlpage/migrations/0001_init.sql").await;
assert!(
resp_result.is_err(),
"Accessing a migration file should be forbidden, but received success: {resp_result:?}"
);
let resp = resp_result.unwrap_err().error_response();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
let srv_resp = actix_web::test::TestRequest::default().to_srv_response(resp);
let body = test::read_body(srv_resp).await;
assert!(
String::from_utf8_lossy(&body)
.to_lowercase()
.contains("forbidden"),
);
}
#[actix_web::test]
async fn test_404_fallback() {
for f in [
"/tests/errors/does_not_exist.sql",
"/tests/errors/does_not_exist.html",
"/tests/errors/does_not_exist/",
] {
let resp_result = req_path(f).await;
let resp = resp_result.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK, "{f} isnt 200");
let body = test::read_body(resp).await;
assert!(body.starts_with(b"<!DOCTYPE html>"));
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("But the "));
assert!(body.contains("404.sql"));
assert!(body.contains("file saved the day!"));
assert!(!body.contains("error"));
}
}
#[actix_web::test]
async fn test_default_404() {
for f in [
"/i-do-not-exist.html",
"/i-do-not-exist.sql",
"/i-do-not-exist/",
] {
let resp_result = req_path(f).await;
let resp = resp_result.unwrap();
assert_eq!(
resp.status(),
http::StatusCode::NOT_FOUND,
"{f} should return 404"
);
let body = test::read_body(resp).await;
assert!(body.starts_with(b"<!DOCTYPE html>"));
let body = String::from_utf8(body.to_vec()).unwrap();
let msg = "The page you were looking for does not exist";
assert!(
body.contains(msg),
"{f} should contain '{msg}' but got:\n{body}"
);
assert!(!body.contains("error"));
}
}
#[actix_web::test]
async fn test_default_404_with_redirect() {
let resp_result = req_path("/i-do-not-exist").await;
let resp = resp_result.unwrap();
assert_eq!(
resp.status(),
http::StatusCode::NOT_FOUND,
"/i-do-not-exist should return 404"
);
let resp_result = req_path("/i-do-not-exist/").await;
let resp = resp_result.unwrap();
assert_eq!(
resp.status(),
http::StatusCode::NOT_FOUND,
"/i-do-not-exist/ should return 404"
);
let body = test::read_body(resp).await;
assert!(body.starts_with(b"<!DOCTYPE html>"));
let body = String::from_utf8(body.to_vec()).unwrap();
let msg = "The page you were looking for does not exist";
assert!(
body.contains(msg),
"/i-do-not-exist/ should contain '{msg}' but got:\n{body}"
);
assert!(!body.contains("error"));
}
#[actix_web::test]
async fn test_default_404_when_request_path_descends_into_file() {
let resp_result = req_path("/tests/it_works.txt/site/wp-includes/wlwmanifest.xml").await;
let resp = resp_result.unwrap();
assert_eq!(
resp.status(),
http::StatusCode::NOT_FOUND,
"descending into a file path should behave like a missing resource"
);
let body = test::read_body(resp).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("The page you were looking for does not exist"));
assert!(!body.contains("error"));
}