fix: security audit — patch vulnerabilities V-02 through V-16

- V-02: XSS via innerHTML in profile.js — wrap err.message in escapeHtml()
- V-03: IDOR upload to other users' folders — add folder ownership check
- V-04: IDOR create folders in other users' trees — add parent ownership check
- V-06: Content-Disposition header injection — RFC 5987 percent-encoding
- V-08: WebDAV MOVE/COPY destination without ownership — add assert_owner checks
- V-09: .gitignore missing cert/key patterns — add *.pem, *.key, *.p12, etc.
- V-11: Username accepts XSS payloads — restrict to [a-zA-Z0-9._-]
- V-12: Minimal email validation — reject forbidden chars, require domain dot
- V-13: admin_reset_password doesn't invalidate sessions — revoke all sessions
- V-14: Rate limiting bypassable via X-Forwarded-For — gate behind OXICLOUD_TRUST_PROXY_HEADERS
- V-15: Cookie Secure flag off by default — default to true (safe-by-default)
- V-16: LIKE wildcard injection in searches — add like_escape() helper across 9 sites
This commit is contained in:
Dionisio
2026-03-05 14:52:11 +01:00
parent b503e08384
commit 33cfb0faef
14 changed files with 265 additions and 58 deletions
+24 -13
View File
@@ -89,24 +89,35 @@ impl RateLimiter {
// ─── Axum middleware factories ──────────────────────────────────────────────
/// Extract the most-likely real client IP from headers / connection info.
///
/// Proxy headers (`X-Forwarded-For`, `X-Real-Ip`) are only trusted when
/// `OXICLOUD_TRUST_PROXY_HEADERS=true` is set. Without a trusted reverse
/// proxy in front of the app, an attacker can spoof these headers to bypass
/// rate limiting.
pub fn extract_client_ip<B>(req: &Request<B>) -> String {
let trust_proxy = std::env::var("OXICLOUD_TRUST_PROXY_HEADERS")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
let headers = req.headers();
// 1. X-Forwarded-For (first entry — closest to the client)
if let Some(xff) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok())
&& let Some(first) = xff.split(',').next()
{
let ip = first.trim();
if !ip.is_empty() {
return ip.to_string();
if trust_proxy {
// 1. X-Forwarded-For (first entry — closest to the client)
if let Some(xff) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok())
&& let Some(first) = xff.split(',').next()
{
let ip = first.trim();
if !ip.is_empty() {
return ip.to_string();
}
}
}
// 2. X-Real-Ip
if let Some(xri) = headers.get("x-real-ip").and_then(|v| v.to_str().ok()) {
let ip = xri.trim();
if !ip.is_empty() {
return ip.to_string();
// 2. X-Real-Ip
if let Some(xri) = headers.get("x-real-ip").and_then(|v| v.to_str().ok()) {
let ip = xri.trim();
if !ip.is_empty() {
return ip.to_string();
}
}
}