lgtm
This commit is contained in:
parent
084117cea8
commit
a8de0a614d
36 changed files with 1329 additions and 522 deletions
|
|
@ -9,6 +9,31 @@ use tracing::warn;
|
|||
|
||||
use crate::state::SharedState;
|
||||
|
||||
/// PocketBase API paths the frontend is allowed to reach via /pb/*.
|
||||
/// Everything else (admins API, settings, logs, backups, collection schema,
|
||||
/// arbitrary collection records like checkout_sessions/invites/short_urls)
|
||||
/// is rejected at the proxy layer as defense-in-depth on top of PB's own
|
||||
/// collection rules.
|
||||
fn is_allowed_pb_path(path: &str) -> bool {
|
||||
// Exact paths
|
||||
if matches!(
|
||||
path,
|
||||
"/api/health" | "/api/oauth2-redirect" | "/api/realtime"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Prefix-allowed paths. The trailing slash is intentional — without it,
|
||||
// `/api/collections/users` (the schema endpoint) would match.
|
||||
const ALLOWED_PREFIXES: &[&str] = &[
|
||||
"/api/collections/users/",
|
||||
"/api/collections/saved_searches/",
|
||||
"/api/files/",
|
||||
];
|
||||
ALLOWED_PREFIXES
|
||||
.iter()
|
||||
.any(|prefix| path.starts_with(prefix))
|
||||
}
|
||||
|
||||
/// Dedicated HTTP client for proxying — does not follow redirects so 3xx
|
||||
/// responses are passed through to the browser (needed for OAuth flows).
|
||||
/// No overall timeout because SSE (Server-Sent Events) connections used by
|
||||
|
|
@ -31,6 +56,13 @@ pub async fn proxy_to_pocketbase(
|
|||
|
||||
let path = req.uri().path();
|
||||
let target_path = path.strip_prefix("/pb").unwrap_or(path);
|
||||
if !is_allowed_pb_path(target_path) {
|
||||
warn!(path = %target_path, "Rejected PocketBase proxy request to disallowed path");
|
||||
return Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
}
|
||||
let query = req
|
||||
.uri()
|
||||
.query()
|
||||
|
|
@ -55,20 +87,9 @@ pub async fn proxy_to_pocketbase(
|
|||
}
|
||||
}
|
||||
|
||||
// Forward client IP so PocketBase rate-limits per-user, not per-server.
|
||||
// Prefer existing X-Forwarded-For (from reverse proxy), fall back to X-Real-IP.
|
||||
if let Some(xff) = req.headers().get("x-forwarded-for") {
|
||||
builder = builder.header("X-Forwarded-For", xff.clone());
|
||||
// First IP in the chain is the original client
|
||||
if let Ok(s) = xff.to_str() {
|
||||
if let Some(client_ip) = s.split(',').next().map(str::trim) {
|
||||
builder = builder.header("X-Real-IP", client_ip);
|
||||
}
|
||||
}
|
||||
} else if let Some(real_ip) = req.headers().get("x-real-ip") {
|
||||
builder = builder.header("X-Forwarded-For", real_ip.clone());
|
||||
builder = builder.header("X-Real-IP", real_ip.clone());
|
||||
}
|
||||
// Do not forward client-supplied X-Forwarded-For/X-Real-IP. PocketBase
|
||||
// may use trusted proxy headers for rate limits, so accepting public
|
||||
// values here lets callers choose their own source IP.
|
||||
|
||||
// Forward body
|
||||
let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue