Skip to content

Commit 3fe0260

Browse files
authored
feat(core): allow CSP configuration to be an object, ref #3533 (#3603)
1 parent 141133a commit 3fe0260

File tree

6 files changed

+250
-54
lines changed

6 files changed

+250
-54
lines changed

.changes/object-csp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Allows the configuration CSP to be an object mapping a directive name to its source list.

core/tauri-utils/src/config.rs

Lines changed: 164 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ use serde_json::Value as JsonValue;
2222
use serde_with::skip_serializing_none;
2323
use url::Url;
2424

25-
use std::{collections::HashMap, fmt, fs::read_to_string, path::PathBuf};
25+
use std::{
26+
collections::HashMap,
27+
fmt::{self, Display},
28+
fs::read_to_string,
29+
path::PathBuf,
30+
};
2631

2732
/// Items to help with parsing content into a [`Config`].
2833
pub mod parse;
@@ -593,6 +598,121 @@ fn default_file_drop_enabled() -> bool {
593598
true
594599
}
595600

601+
/// A Content-Security-Policy directive source list.
602+
/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources>.
603+
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
604+
#[cfg_attr(feature = "schema", derive(JsonSchema))]
605+
#[serde(rename_all = "camelCase", untagged)]
606+
pub enum CspDirectiveSources {
607+
/// An inline list of CSP sources. Same as [`Self::List`], but concatenated with a space separator.
608+
Inline(String),
609+
/// A list of CSP sources. The collection will be concatenated with a space separator for the CSP string.
610+
List(Vec<String>),
611+
}
612+
613+
impl Default for CspDirectiveSources {
614+
fn default() -> Self {
615+
Self::List(Vec::new())
616+
}
617+
}
618+
619+
impl From<CspDirectiveSources> for Vec<String> {
620+
fn from(sources: CspDirectiveSources) -> Self {
621+
match sources {
622+
CspDirectiveSources::Inline(source) => source.split(' ').map(|s| s.to_string()).collect(),
623+
CspDirectiveSources::List(l) => l,
624+
}
625+
}
626+
}
627+
628+
impl CspDirectiveSources {
629+
/// Whether the given source is configured on this directive or not.
630+
pub fn contains(&self, source: &str) -> bool {
631+
match self {
632+
Self::Inline(s) => s.contains(&format!("{} ", source)) || s.contains(&format!(" {}", source)),
633+
Self::List(l) => l.contains(&source.into()),
634+
}
635+
}
636+
637+
/// Appends the given source to this directive.
638+
pub fn push<S: AsRef<str>>(&mut self, source: S) {
639+
match self {
640+
Self::Inline(s) => {
641+
s.push(' ');
642+
s.push_str(source.as_ref());
643+
}
644+
Self::List(l) => {
645+
l.push(source.as_ref().to_string());
646+
}
647+
}
648+
}
649+
650+
/// Extends this CSP directive source list with the given array of sources.
651+
pub fn extend(&mut self, sources: Vec<String>) {
652+
for s in sources {
653+
self.push(s);
654+
}
655+
}
656+
}
657+
658+
/// A Content-Security-Policy definition.
659+
/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
660+
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
661+
#[cfg_attr(feature = "schema", derive(JsonSchema))]
662+
#[serde(rename_all = "camelCase", untagged)]
663+
pub enum Csp {
664+
/// The entire CSP policy in a single text string.
665+
Policy(String),
666+
/// An object mapping a directive with its sources values as a list of strings.
667+
DirectiveMap(HashMap<String, CspDirectiveSources>),
668+
}
669+
670+
impl From<HashMap<String, CspDirectiveSources>> for Csp {
671+
fn from(map: HashMap<String, CspDirectiveSources>) -> Self {
672+
Self::DirectiveMap(map)
673+
}
674+
}
675+
676+
impl From<Csp> for HashMap<String, CspDirectiveSources> {
677+
fn from(csp: Csp) -> Self {
678+
match csp {
679+
Csp::Policy(policy) => {
680+
let mut map = HashMap::new();
681+
for directive in policy.split(';') {
682+
let mut tokens = directive.trim().split(' ');
683+
if let Some(directive) = tokens.next() {
684+
let sources = tokens.map(|s| s.to_string()).collect::<Vec<String>>();
685+
map.insert(directive.to_string(), CspDirectiveSources::List(sources));
686+
}
687+
}
688+
map
689+
}
690+
Csp::DirectiveMap(m) => m,
691+
}
692+
}
693+
}
694+
695+
impl Display for Csp {
696+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
697+
match self {
698+
Self::Policy(s) => write!(f, "{}", s),
699+
Self::DirectiveMap(m) => {
700+
let len = m.len();
701+
let mut i = 0;
702+
for (directive, sources) in m {
703+
let sources: Vec<String> = sources.clone().into();
704+
write!(f, "{} {}", directive, sources.join(" "))?;
705+
i += 1;
706+
if i != len {
707+
write!(f, "; ")?;
708+
}
709+
}
710+
Ok(())
711+
}
712+
}
713+
}
714+
}
715+
596716
/// Security configuration.
597717
#[skip_serializing_none]
598718
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
@@ -604,12 +724,12 @@ pub struct SecurityConfig {
604724
///
605725
/// This is a really important part of the configuration since it helps you ensure your WebView is secured.
606726
/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
607-
pub csp: Option<String>,
727+
pub csp: Option<Csp>,
608728
/// The Content Security Policy that will be injected on all HTML files on development.
609729
///
610730
/// This is a really important part of the configuration since it helps you ensure your WebView is secured.
611731
/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
612-
pub dev_csp: Option<String>,
732+
pub dev_csp: Option<Csp>,
613733
/// Freeze the `Object.prototype` when using the custom protocol.
614734
#[serde(default)]
615735
pub freeze_prototype: bool,
@@ -2399,10 +2519,49 @@ mod build {
23992519
}
24002520
}
24012521

2522+
impl ToTokens for CspDirectiveSources {
2523+
fn to_tokens(&self, tokens: &mut TokenStream) {
2524+
let prefix = quote! { ::tauri::utils::config::CspDirectiveSources };
2525+
2526+
tokens.append_all(match self {
2527+
Self::Inline(sources) => {
2528+
let sources = sources.as_str();
2529+
quote!(#prefix::Inline(#sources.into()))
2530+
}
2531+
Self::List(list) => {
2532+
let list = vec_lit(list, str_lit);
2533+
quote!(#prefix::List(#list))
2534+
}
2535+
})
2536+
}
2537+
}
2538+
2539+
impl ToTokens for Csp {
2540+
fn to_tokens(&self, tokens: &mut TokenStream) {
2541+
let prefix = quote! { ::tauri::utils::config::Csp };
2542+
2543+
tokens.append_all(match self {
2544+
Self::Policy(policy) => {
2545+
let policy = policy.as_str();
2546+
quote!(#prefix::Policy(#policy.into()))
2547+
}
2548+
Self::DirectiveMap(list) => {
2549+
let map = map_lit(
2550+
quote! { ::std::collections::HashMap },
2551+
list,
2552+
str_lit,
2553+
identity,
2554+
);
2555+
quote!(#prefix::DirectiveMap(#map))
2556+
}
2557+
})
2558+
}
2559+
}
2560+
24022561
impl ToTokens for SecurityConfig {
24032562
fn to_tokens(&self, tokens: &mut TokenStream) {
2404-
let csp = opt_str_lit(self.csp.as_ref());
2405-
let dev_csp = opt_str_lit(self.dev_csp.as_ref());
2563+
let csp = opt_lit(self.csp.as_ref());
2564+
let dev_csp = opt_lit(self.dev_csp.as_ref());
24062565
let freeze_prototype = self.freeze_prototype;
24072566

24082567
literal_struct!(tokens, SecurityConfig, csp, dev_csp, freeze_prototype);

core/tauri/src/manager.rs

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use tauri_macros::default_runtime;
2020
use tauri_utils::pattern::isolation::RawIsolationPayload;
2121
use tauri_utils::{
2222
assets::{AssetKey, CspHash},
23+
config::{Csp, CspDirectiveSources},
2324
html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
2425
};
2526

@@ -67,8 +68,8 @@ const MENU_EVENT: &str = "tauri://menu";
6768
#[derive(Default)]
6869
/// Spaced and quoted Content-Security-Policy hash values.
6970
struct CspHashStrings {
70-
script: String,
71-
style: String,
71+
script: Vec<String>,
72+
style: Vec<String>,
7273
}
7374

7475
/// Sets the CSP value to the asset HTML if needed (on Linux).
@@ -78,20 +79,19 @@ fn set_csp<R: Runtime>(
7879
assets: Arc<dyn Assets>,
7980
asset_path: &AssetKey,
8081
#[allow(unused_variables)] manager: &WindowManager<R>,
81-
mut csp: String,
82+
csp: Csp,
8283
) -> String {
84+
let mut csp = csp.into();
8385
let hash_strings =
8486
assets
8587
.csp_hashes(asset_path)
8688
.fold(CspHashStrings::default(), |mut acc, hash| {
8789
match hash {
8890
CspHash::Script(hash) => {
89-
acc.script.push(' ');
90-
acc.script.push_str(hash);
91+
acc.script.push(hash.into());
9192
}
9293
CspHash::Style(hash) => {
93-
acc.style.push(' ');
94-
acc.style.push_str(hash);
94+
acc.style.push(hash.into());
9595
}
9696
_csp_hash => {
9797
#[cfg(debug_assertions)]
@@ -120,15 +120,13 @@ fn set_csp<R: Runtime>(
120120

121121
#[cfg(feature = "isolation")]
122122
if let Pattern::Isolation { schema, .. } = &manager.inner.pattern {
123-
let default_src = format!("default-src {}", format_real_schema(schema));
124-
if csp.contains("default-src") {
125-
csp = csp.replace("default-src", &default_src);
126-
} else {
127-
csp.push_str("; ");
128-
csp.push_str(&default_src);
129-
}
123+
let default_src = csp
124+
.entry("default-src".into())
125+
.or_insert_with(Default::default);
126+
default_src.push(format_real_schema(schema));
130127
}
131128

129+
let csp = Csp::DirectiveMap(csp).to_string();
132130
#[cfg(target_os = "linux")]
133131
{
134132
*asset = asset.replacen(tauri_utils::html::CSP_TOKEN, &csp, 1);
@@ -156,9 +154,9 @@ fn replace_with_callback<F: FnMut() -> String>(
156154
fn replace_csp_nonce(
157155
asset: &mut String,
158156
token: &str,
159-
csp: &mut String,
160-
csp_attr: &str,
161-
hashes: String,
157+
csp: &mut HashMap<String, CspDirectiveSources>,
158+
directive: &str,
159+
hashes: Vec<String>,
162160
) {
163161
let mut nonces = Vec::new();
164162
*asset = replace_with_callback(asset, token, || {
@@ -168,29 +166,17 @@ fn replace_csp_nonce(
168166
});
169167

170168
if !(nonces.is_empty() && hashes.is_empty()) {
171-
let attr = format!(
172-
"{} 'self'{}{}",
173-
csp_attr,
174-
if nonces.is_empty() {
175-
"".into()
176-
} else {
177-
format!(
178-
" {}",
179-
nonces
180-
.into_iter()
181-
.map(|n| format!("'nonce-{}'", n))
182-
.collect::<Vec<String>>()
183-
.join(" ")
184-
)
185-
},
186-
hashes
187-
);
188-
if csp.contains(csp_attr) {
189-
*csp = csp.replace(csp_attr, &attr);
190-
} else {
191-
csp.push_str("; ");
192-
csp.push_str(&attr);
169+
let nonce_sources = nonces
170+
.into_iter()
171+
.map(|n| format!("'nonce-{}'", n))
172+
.collect::<Vec<String>>();
173+
let sources = csp.entry(directive.into()).or_insert_with(Default::default);
174+
let self_source = "'self'".to_string();
175+
if !sources.contains(&self_source) {
176+
sources.push(self_source);
193177
}
178+
sources.extend(nonce_sources);
179+
sources.extend(hashes);
194180
}
195181
}
196182

@@ -376,7 +362,7 @@ impl<R: Runtime> WindowManager<R> {
376362
}
377363
}
378364

379-
fn csp(&self) -> Option<String> {
365+
fn csp(&self) -> Option<Csp> {
380366
if cfg!(feature = "custom-protocol") {
381367
self.inner.config.tauri.security.csp.clone()
382368
} else {
@@ -1045,7 +1031,7 @@ impl<R: Runtime> WindowManager<R> {
10451031
// naive way to check if it's an html
10461032
if html.contains('<') && html.contains('>') {
10471033
let mut document = tauri_utils::html::parse(html);
1048-
tauri_utils::html::inject_csp(&mut document, &csp);
1034+
tauri_utils::html::inject_csp(&mut document, &csp.to_string());
10491035
url.set_path(&format!("text/html,{}", document.to_string()));
10501036
}
10511037
}

examples/api/src-tauri/tauri.conf.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@
122122
}
123123
],
124124
"security": {
125-
"csp": "default-src 'self' customprotocol: asset: img-src: 'self'; style-src 'unsafe-inline' 'self' https://fonts.googleapis.com; img-src 'self' asset: https://asset.localhost blob: data:; font-src https://fonts.gstatic.com",
125+
"csp": {
126+
"default-src": "'self' customprotocol: asset:",
127+
"font-src": ["https://fonts.gstatic.com"],
128+
"img-src": "'self' asset: https://asset.localhost blob: data:",
129+
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
130+
},
126131
"freezePrototype": true
127132
},
128133
"systemTray": {

0 commit comments

Comments
 (0)