Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/channels/web/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,21 @@ pub async fn start_server(
header::X_FRAME_OPTIONS,
header::HeaderValue::from_static("DENY"),
))
.layer(SetResponseHeaderLayer::if_not_present(
header::HeaderName::from_static("content-security-policy"),
header::HeaderValue::from_static(
"default-src 'self'; \
script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; \
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The style-src directive includes 'unsafe-inline', which allows the use of inline <style> blocks and style attributes. This is a potential security risk as it can be a vector for Cross-Site Scripting (XSS) if an attacker finds a way to inject content into the page.

This is likely needed due to style="display: none;" attributes in src/channels/web/static/index.html. To improve security, I recommend refactoring the frontend code to use a CSS class for controlling visibility instead of inline styles. For example:

CSS:

.hidden {
  display: none;
}

JavaScript:

// To hide
element.classList.add('hidden');

// To show
element.classList.remove('hidden');

This change would allow you to remove 'unsafe-inline' from the CSP, making it more robust.

Suggested change
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
style-src 'self' https://fonts.googleapis.com; \

font-src https://fonts.gstatic.com; \
connect-src 'self'; \
img-src 'self' data:; \
object-src 'none'; \
frame-ancestors 'none'; \
base-uri 'self'; \
form-action 'self'",
),
))
.with_state(state.clone());

let (shutdown_tx, shutdown_rx) = oneshot::channel();
Expand Down Expand Up @@ -2749,6 +2764,56 @@ mod tests {
.with_state(state)
}

#[tokio::test]
async fn test_csp_header_present_on_responses() {
use std::net::SocketAddr;

let state = test_gateway_state(None);

let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let bound = start_server(addr, state.clone(), "test-token".to_string())
.await
.expect("server should start");

let client = reqwest::Client::new();
let resp = client
.get(format!("http://{}/api/health", bound))
.send()
.await
.expect("health request should succeed");

assert_eq!(resp.status(), 200);

let csp = resp
.headers()
.get("content-security-policy")
.expect("CSP header must be present");

let csp_str = csp.to_str().expect("CSP header should be valid UTF-8");
assert!(
csp_str.contains("default-src 'self'"),
"CSP must contain default-src"
);
assert!(
csp_str.contains(
"script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com"
),
"CSP must allow both marked and DOMPurify script CDNs"
);
assert!(
csp_str.contains("object-src 'none'"),
"CSP must contain object-src 'none'"
);
assert!(
csp_str.contains("frame-ancestors 'none'"),
"CSP must contain frame-ancestors 'none'"
);

if let Some(tx) = state.shutdown_tx.write().await.take() {
let _ = tx.send(());
}
}

#[tokio::test]
async fn test_oauth_callback_missing_params() {
use axum::body::Body;
Expand Down
Loading