chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:57 +08:00
commit d718c5a372
986 changed files with 74597 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
SELECT 'alert' AS component,
'We almost got an oopsie' AS title,
'But the `404.sql` file saved the day!' AS description_md;
+43
View File
@@ -0,0 +1,43 @@
use crate::common::{get_request_to, req_path};
use actix_web::{http::StatusCode, test};
use sqlpage::webserver::http::main_handler;
#[actix_web::test]
async fn test_basic_auth_not_provided() {
let resp_result = req_path("/tests/errors/basic_auth.sql").await;
let resp = resp_result.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
assert_eq!(
resp.headers().get("www-authenticate").unwrap(),
"Basic realm=\"Authentication required\", charset=\"UTF-8\""
);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("Unauthorized"),
"{body_str}\nexpected to contain Unauthorized"
);
assert!(
!body_str.contains("Success!"),
"{body_str}\nexpected not to contain Success!"
);
}
#[actix_web::test]
async fn test_basic_auth_with_credentials() {
let req = get_request_to("/tests/errors/basic_auth.sql")
.await
.unwrap() // log in with credentials "user:password"
.append_header(("Authorization", "Basic dXNlcjpwYXNzd29yZA=="))
.to_srv_request();
let resp = main_handler(req)
.await
.expect("req with credentials should succeed");
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.contains("Success!"),
"{body_str}\nexpected to contain Success"
);
}
+5
View File
@@ -0,0 +1,5 @@
SELECT 'authentication' AS component,
'$argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$oKBq5E8XFTHO2w' AS password_hash, -- this is a hash of the password 'password'
sqlpage.basic_auth_password() AS password; -- this is the password that the user entered in the browser popup
SELECT 'text' AS component, 'Success!' AS contents;
+108
View File
@@ -0,0 +1,108 @@
use crate::common::req_path;
use actix_web::{http::StatusCode, test};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde_json::{Value, json};
struct InvalidHeaderCase {
name: &'static str,
properties: Value,
}
async fn assert_invalid_header_response(case: &InvalidHeaderCase) {
let properties = serde_json::to_string(&case.properties).unwrap();
let properties = utf8_percent_encode(&properties, NON_ALPHANUMERIC).to_string();
let path = format!("/tests/errors/invalid_header.sql?properties={properties}");
let resp = req_path(&path).await.unwrap_or_else(|err| {
panic!(
"{} should return an error response instead of failing the request: {err:#}",
case.name
)
});
assert_eq!(
resp.status(),
StatusCode::INTERNAL_SERVER_ERROR,
"{} should return a 500 response",
case.name
);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(
body_str.to_lowercase().contains("error"),
"{} should render an error response body, got:\n{body_str}",
case.name
);
}
#[actix_web::test]
async fn test_invalid_header_components_return_an_error_response() {
let cases = vec![
InvalidHeaderCase {
name: "cookie domain with newline",
properties: json!({
"component": "cookie",
"name": "boom",
"value": "x",
"domain": "\n",
}),
},
InvalidHeaderCase {
name: "cookie path with DEL",
properties: json!({
"component": "cookie",
"name": "boom",
"value": "x",
"path": "\u{007f}",
}),
},
InvalidHeaderCase {
name: "http_header value with carriage return",
properties: json!({
"component": "http_header",
"X-Test": "\r",
}),
},
InvalidHeaderCase {
name: "redirect link with NUL",
properties: json!({
"component": "redirect",
"link": "\u{0000}",
}),
},
InvalidHeaderCase {
name: "authentication link with unit separator",
properties: json!({
"component": "authentication",
"link": "\u{001f}",
}),
},
InvalidHeaderCase {
name: "download filename with newline",
properties: json!({
"component": "download",
"data_url": "data:text/plain,ok",
"filename": "\n",
}),
},
InvalidHeaderCase {
name: "csv filename with carriage return",
properties: json!({
"component": "csv",
"filename": "\r",
}),
},
InvalidHeaderCase {
name: "csv title with NUL",
properties: json!({
"component": "csv",
"title": "\u{0000}",
}),
},
];
for case in &cases {
assert_invalid_header_response(case).await;
}
}
+1
View File
@@ -0,0 +1 @@
select 'dynamic' as component, $properties as properties;
+175
View File
@@ -0,0 +1,175 @@
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"));
}
+4
View File
@@ -0,0 +1,4 @@
-- Trusted page: loads a reserved (sqlpage/) file via the privileged run_sql,
-- which populates the shared sql_file_cache with the parsed private file.
select 'dynamic' as component,
sqlpage.run_sql('sqlpage/private_cache_bypass_test.sql') as properties;