138 lines
4.3 KiB
Rust
138 lines
4.3 KiB
Rust
/*
|
|
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
|
* Source-Available Code / Dual-Licensed.
|
|
*
|
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
|
* Commercial or production use requires a valid paid license.
|
|
* See LICENSE file in repository root for details.
|
|
*/
|
|
|
|
use super::*;
|
|
|
|
fn websocket_headers(origin: &'static str, host: &'static str) -> HeaderMap {
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(header::ORIGIN, HeaderValue::from_static(origin));
|
|
headers.insert(header::HOST, HeaderValue::from_static(host));
|
|
headers
|
|
}
|
|
|
|
#[test]
|
|
fn account_sessions_are_cookie_only() {
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
header::AUTHORIZATION,
|
|
HeaderValue::from_static("Bearer legacy-account-token"),
|
|
);
|
|
assert_eq!(session_token(&headers), None);
|
|
assert_eq!(bearer_token(&headers), Some("legacy-account-token"));
|
|
|
|
headers.insert(
|
|
header::COOKIE,
|
|
HeaderValue::from_static("__Host-rustpad_session=cookie-token"),
|
|
);
|
|
assert_eq!(session_token(&headers), Some("cookie-token"));
|
|
}
|
|
|
|
#[test]
|
|
fn prefers_proxy_controlled_real_ip() {
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
axum::http::HeaderName::from_static("x-forwarded-for"),
|
|
HeaderValue::from_static("203.0.113.10"),
|
|
);
|
|
headers.insert(
|
|
axum::http::HeaderName::from_static("x-real-ip"),
|
|
HeaderValue::from_static("198.51.100.20"),
|
|
);
|
|
assert_eq!(client_key(&headers), "ip:198.51.100.20");
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_same_origin_websocket() {
|
|
let headers = websocket_headers("https://pad.example.com", "pad.example.com");
|
|
assert!(websocket_origin_allowed(&headers));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_cross_origin_websocket() {
|
|
let headers = websocket_headers("https://evil.example", "pad.example.com");
|
|
assert!(!websocket_origin_allowed(&headers));
|
|
}
|
|
|
|
#[test]
|
|
fn does_not_trust_forwarded_host_for_websocket_origin() {
|
|
let mut headers = websocket_headers("https://evil.example", "pad.example.com");
|
|
headers.insert(
|
|
axum::http::HeaderName::from_static("x-forwarded-host"),
|
|
HeaderValue::from_static("evil.example"),
|
|
);
|
|
assert!(!websocket_origin_allowed(&headers));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_origin_with_path() {
|
|
let headers = websocket_headers("https://pad.example.com/other", "pad.example.com");
|
|
assert!(!websocket_origin_allowed(&headers));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_websocket_origin() {
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(header::HOST, HeaderValue::from_static("pad.example.com"));
|
|
assert!(!websocket_origin_allowed(&headers));
|
|
}
|
|
|
|
#[test]
|
|
fn secure_cookies_are_not_script_readable() {
|
|
let value = session_cookie("abc123", 7).to_str().unwrap();
|
|
assert!(value.contains("HttpOnly"));
|
|
assert!(value.contains("Secure"));
|
|
assert!(value.contains("SameSite=Lax"));
|
|
assert!(value.starts_with("__Host-rustpad_session=abc123;"));
|
|
}
|
|
|
|
#[test]
|
|
fn share_sessions_use_separate_scoped_opaque_cookies() {
|
|
let value = share_session_cookie("workspace", "private-space", "opaque", 600)
|
|
.to_str()
|
|
.unwrap();
|
|
assert!(value.starts_with("__Host-rustpad_share_"));
|
|
assert!(value.contains("=opaque;"));
|
|
assert!(value.contains("Max-Age=600"));
|
|
assert!(value.contains("HttpOnly"));
|
|
assert!(value.contains("Secure"));
|
|
assert!(value.contains("SameSite=Lax"));
|
|
}
|
|
|
|
#[test]
|
|
fn csrf_requires_matching_cookie_and_header() {
|
|
let token = "a".repeat(CSRF_TOKEN_BYTES * 2);
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
header::COOKIE,
|
|
HeaderValue::from_str(&format!("{CSRF_COOKIE}={token}")).unwrap(),
|
|
);
|
|
headers.insert(
|
|
axum::http::HeaderName::from_static(CSRF_HEADER),
|
|
HeaderValue::from_str(&token).unwrap(),
|
|
);
|
|
assert!(csrf_request_is_valid(&headers));
|
|
|
|
headers.insert(
|
|
axum::http::HeaderName::from_static(CSRF_HEADER),
|
|
HeaderValue::from_static(
|
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
),
|
|
);
|
|
assert!(!csrf_request_is_valid(&headers));
|
|
}
|
|
|
|
#[test]
|
|
fn csrf_cookie_is_strict_and_script_readable() {
|
|
let token = "a".repeat(CSRF_TOKEN_BYTES * 2);
|
|
let value = csrf_cookie(&token).to_str().unwrap();
|
|
assert!(value.contains("Secure"));
|
|
assert!(value.contains("SameSite=Strict"));
|
|
assert!(!value.contains("HttpOnly"));
|
|
}
|