mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(app-proxy): allow extra CSP sources from the environment (#1652)
This commit is contained in:
@@ -31,6 +31,14 @@ [email protected]
|
||||
#FLUXER_PASSKEY_RP_NAME=Fluxer
|
||||
#FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS=https://chat.example.com
|
||||
|
||||
# Extra Content-Security-Policy sources, appended to the built-in ones. Set these
|
||||
# only when a browser must reach an origin the defaults do not cover, such as a
|
||||
# voice server hosted on a domain other than FLUXER_DOMAIN. Separate several
|
||||
# sources with spaces or commas.
|
||||
#FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com:7881
|
||||
#FLUXER_CSP_EXTRA_IMG_SRC=https://cdn.example.com
|
||||
#FLUXER_CSP_EXTRA_SCRIPT_SRC=https://analytics.example.com
|
||||
|
||||
LIVEKIT_API_KEY=fluxer
|
||||
LIVEKIT_API_SECRET=CHANGE_ME
|
||||
|
||||
|
||||
@@ -285,6 +285,17 @@ services:
|
||||
DISCOVERY_UPSTREAM_URL: http://caddy:8088/api/.well-known/fluxer
|
||||
PUBLIC_BOOTSTRAP_API_ENDPOINT: /api
|
||||
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/api
|
||||
FLUXER_CSP_EXTRA_DEFAULT_SRC: ${FLUXER_CSP_EXTRA_DEFAULT_SRC:-}
|
||||
FLUXER_CSP_EXTRA_CONNECT_SRC: ${FLUXER_CSP_EXTRA_CONNECT_SRC:-}
|
||||
FLUXER_CSP_EXTRA_IMG_SRC: ${FLUXER_CSP_EXTRA_IMG_SRC:-}
|
||||
FLUXER_CSP_EXTRA_MEDIA_SRC: ${FLUXER_CSP_EXTRA_MEDIA_SRC:-}
|
||||
FLUXER_CSP_EXTRA_FONT_SRC: ${FLUXER_CSP_EXTRA_FONT_SRC:-}
|
||||
FLUXER_CSP_EXTRA_SCRIPT_SRC: ${FLUXER_CSP_EXTRA_SCRIPT_SRC:-}
|
||||
FLUXER_CSP_EXTRA_STYLE_SRC: ${FLUXER_CSP_EXTRA_STYLE_SRC:-}
|
||||
FLUXER_CSP_EXTRA_FRAME_SRC: ${FLUXER_CSP_EXTRA_FRAME_SRC:-}
|
||||
FLUXER_CSP_EXTRA_WORKER_SRC: ${FLUXER_CSP_EXTRA_WORKER_SRC:-}
|
||||
FLUXER_CSP_EXTRA_MANIFEST_SRC: ${FLUXER_CSP_EXTRA_MANIFEST_SRC:-}
|
||||
FLUXER_CSP_REPORT_URI: ${FLUXER_CSP_REPORT_URI:-}
|
||||
depends_on:
|
||||
api: {condition: service_healthy}
|
||||
caddy: {condition: service_started}
|
||||
|
||||
@@ -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]
|
||||
|
||||
+30
-14
@@ -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 {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user