fix(app-proxy): allow extra CSP sources from the environment (#1652)

This commit is contained in:
Hampus
2026-08-15 21:32:16 +02:00
committed by GitHub
parent f45a3073c1
commit 562819be09
4 changed files with 98 additions and 28 deletions
+48 -13
View File
@@ -87,19 +87,52 @@ impl ReleaseChannel {
#[derive(Clone, Debug, Default)]
pub struct CspConfig {
pub default_src: Option<Vec<String>>,
pub connect_src: Option<Vec<String>>,
pub img_src: Option<Vec<String>>,
pub media_src: Option<Vec<String>>,
pub font_src: Option<Vec<String>>,
pub script_src: Option<Vec<String>>,
pub style_src: Option<Vec<String>>,
pub frame_src: Option<Vec<String>>,
pub worker_src: Option<Vec<String>>,
pub manifest_src: Option<Vec<String>>,
pub extra_default_src: Option<Vec<String>>,
pub extra_connect_src: Option<Vec<String>>,
pub extra_img_src: Option<Vec<String>>,
pub extra_media_src: Option<Vec<String>>,
pub extra_font_src: Option<Vec<String>>,
pub extra_script_src: Option<Vec<String>>,
pub extra_style_src: Option<Vec<String>>,
pub extra_frame_src: Option<Vec<String>>,
pub extra_worker_src: Option<Vec<String>>,
pub extra_manifest_src: Option<Vec<String>>,
pub report_uri: Option<String>,
}
impl CspConfig {
pub fn from_env() -> Self {
Self {
extra_default_src: read_csp_sources("FLUXER_CSP_EXTRA_DEFAULT_SRC"),
extra_connect_src: read_csp_sources("FLUXER_CSP_EXTRA_CONNECT_SRC"),
extra_img_src: read_csp_sources("FLUXER_CSP_EXTRA_IMG_SRC"),
extra_media_src: read_csp_sources("FLUXER_CSP_EXTRA_MEDIA_SRC"),
extra_font_src: read_csp_sources("FLUXER_CSP_EXTRA_FONT_SRC"),
extra_script_src: read_csp_sources("FLUXER_CSP_EXTRA_SCRIPT_SRC"),
extra_style_src: read_csp_sources("FLUXER_CSP_EXTRA_STYLE_SRC"),
extra_frame_src: read_csp_sources("FLUXER_CSP_EXTRA_FRAME_SRC"),
extra_worker_src: read_csp_sources("FLUXER_CSP_EXTRA_WORKER_SRC"),
extra_manifest_src: read_csp_sources("FLUXER_CSP_EXTRA_MANIFEST_SRC"),
report_uri: cfg::non_empty_env("FLUXER_CSP_REPORT_URI"),
}
}
}
fn read_csp_sources(name: &str) -> Option<Vec<String>> {
let sources: Vec<String> = cfg::read_env(name, "")
.split([',', ' ', '\t', '\n'])
.map(str::trim)
.filter(|source| !source.is_empty())
.map(str::to_owned)
.collect();
if sources.is_empty() {
None
} else {
Some(sources)
}
}
impl AppProxyConfig {
pub fn from_env() -> Self {
let release_channel = ReleaseChannel::from_env_value(&cfg::read_env_preferred(
@@ -169,7 +202,7 @@ impl AppProxyConfig {
bootstrap_api_public_endpoint: cfg::non_empty_env(
"PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT",
),
csp: CspConfig::default(),
csp: CspConfig::from_env(),
geoip_source,
geoip_s3_config,
trust_client_ip_header: cfg::read_bool_env(
@@ -305,9 +338,11 @@ mod tests {
}
#[test]
fn csp_config_default_has_no_overrides() {
fn csp_config_default_has_no_extra_sources() {
let c = CspConfig::default();
assert!(c.default_src.is_none() && c.script_src.is_none() && c.report_uri.is_none());
assert!(
c.extra_default_src.is_none() && c.extra_script_src.is_none() && c.report_uri.is_none()
);
}
#[test]
+31 -15
View File
@@ -97,7 +97,7 @@ fn build_csp_directives(
let mut directives = Vec::with_capacity(14);
let mut default = vec!["'self'".to_owned()];
extend_from(&mut default, config.default_src.as_deref(), &[]);
extend_from(&mut default, config.extra_default_src.as_deref(), &[]);
directives.push(format!("default-src {}", default.join(" ")));
let mut script = vec![
@@ -108,17 +108,21 @@ fn build_csp_directives(
if let Some(n) = nonce {
script.insert(1, format!("'nonce-{n}'"));
}
extend_from(&mut script, config.script_src.as_deref(), SCRIPT_SOURCES);
extend_from(
&mut script,
config.extra_script_src.as_deref(),
SCRIPT_SOURCES,
);
extend_runtime_sources(&mut script, runtime_sources, true, false);
directives.push(format!("script-src {}", script.join(" ")));
let mut style = vec!["'self'".to_owned(), "'unsafe-inline'".to_owned()];
extend_from(&mut style, config.style_src.as_deref(), STYLE_SOURCES);
extend_from(&mut style, config.extra_style_src.as_deref(), STYLE_SOURCES);
extend_runtime_sources(&mut style, runtime_sources, true, true);
directives.push(format!("style-src {}", style.join(" ")));
let mut img = vec!["'self'".to_owned(), "blob:".to_owned(), "data:".to_owned()];
extend_from(&mut img, config.img_src.as_deref(), IMAGE_SOURCES);
extend_from(&mut img, config.extra_img_src.as_deref(), IMAGE_SOURCES);
extend_runtime_sources(&mut img, runtime_sources, true, true);
for origin in &runtime_sources.branding_image_origins {
push_endpoint_source(&mut img, Some(origin));
@@ -126,34 +130,42 @@ fn build_csp_directives(
directives.push(format!("img-src {}", img.join(" ")));
let mut media = vec!["'self'".to_owned(), "blob:".to_owned()];
extend_from(&mut media, config.media_src.as_deref(), MEDIA_SOURCES);
extend_from(&mut media, config.extra_media_src.as_deref(), MEDIA_SOURCES);
extend_runtime_sources(&mut media, runtime_sources, true, true);
directives.push(format!("media-src {}", media.join(" ")));
let mut font = vec!["'self'".to_owned(), "data:".to_owned()];
extend_from(&mut font, config.font_src.as_deref(), FONT_SOURCES);
extend_from(&mut font, config.extra_font_src.as_deref(), FONT_SOURCES);
extend_runtime_sources(&mut font, runtime_sources, true, true);
directives.push(format!("font-src {}", font.join(" ")));
let mut connect = vec!["'self'".to_owned(), "data:".to_owned()];
extend_from(&mut connect, config.connect_src.as_deref(), CONNECT_SOURCES);
extend_from(
&mut connect,
config.extra_connect_src.as_deref(),
CONNECT_SOURCES,
);
extend_runtime_sources(&mut connect, runtime_sources, true, true);
extend_runtime_s3_sources(&mut connect, runtime_sources);
directives.push(format!("connect-src {}", connect.join(" ")));
let mut frame = vec!["'self'".to_owned()];
extend_from(&mut frame, config.frame_src.as_deref(), FRAME_SOURCES);
extend_from(&mut frame, config.extra_frame_src.as_deref(), FRAME_SOURCES);
directives.push(format!("frame-src {}", frame.join(" ")));
let mut worker = vec!["'self'".to_owned(), "blob:".to_owned()];
extend_from(&mut worker, config.worker_src.as_deref(), WORKER_SOURCES);
extend_from(
&mut worker,
config.extra_worker_src.as_deref(),
WORKER_SOURCES,
);
extend_runtime_sources(&mut worker, runtime_sources, true, false);
directives.push(format!("worker-src {}", worker.join(" ")));
let mut manifest = vec!["'self'".to_owned()];
extend_from(
&mut manifest,
config.manifest_src.as_deref(),
config.extra_manifest_src.as_deref(),
MANIFEST_SOURCES,
);
extend_runtime_sources(&mut manifest, runtime_sources, true, false);
@@ -243,11 +255,15 @@ fn s3_uploads_bucket_origin(runtime_sources: &RuntimeCspSources) -> Option<Strin
Some(format!("{scheme}://{host}{port}"))
}
fn extend_from(target: &mut Vec<String>, overrides: Option<&[String]>, defaults: &[&str]) {
if let Some(sources) = overrides {
target.extend(sources.iter().cloned());
} else {
target.extend(defaults.iter().map(|s| (*s).to_owned()));
fn extend_from(target: &mut Vec<String>, extra: Option<&[String]>, defaults: &[&str]) {
target.extend(defaults.iter().map(|s| (*s).to_owned()));
for source in extra.into_iter().flatten() {
let source = source.trim();
if source.is_empty() || target.iter().any(|existing| existing == source) {
continue;
}
target.push(source.to_owned());
}
}