feat(core): allow CSP configuration to be an object, ref #3533 (#3603)

This commit is contained in:
Lucas Fernandes Nogueira 2022-03-04 21:18:39 -03:00 committed by GitHub
parent 141133a414
commit 3fe0260f4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 250 additions and 54 deletions

5
.changes/object-csp.md Normal file
View File

@ -0,0 +1,5 @@
---
"tauri": patch
---
Allows the configuration CSP to be an object mapping a directive name to its source list.

View File

@ -22,7 +22,12 @@ use serde_json::Value as JsonValue;
use serde_with::skip_serializing_none;
use url::Url;
use std::{collections::HashMap, fmt, fs::read_to_string, path::PathBuf};
use std::{
collections::HashMap,
fmt::{self, Display},
fs::read_to_string,
path::PathBuf,
};
/// Items to help with parsing content into a [`Config`].
pub mod parse;
@ -593,6 +598,121 @@ fn default_file_drop_enabled() -> bool {
true
}
/// A Content-Security-Policy directive source list.
/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources>.
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", untagged)]
pub enum CspDirectiveSources {
/// An inline list of CSP sources. Same as [`Self::List`], but concatenated with a space separator.
Inline(String),
/// A list of CSP sources. The collection will be concatenated with a space separator for the CSP string.
List(Vec<String>),
}
impl Default for CspDirectiveSources {
fn default() -> Self {
Self::List(Vec::new())
}
}
impl From<CspDirectiveSources> for Vec<String> {
fn from(sources: CspDirectiveSources) -> Self {
match sources {
CspDirectiveSources::Inline(source) => source.split(' ').map(|s| s.to_string()).collect(),
CspDirectiveSources::List(l) => l,
}
}
}
impl CspDirectiveSources {
/// Whether the given source is configured on this directive or not.
pub fn contains(&self, source: &str) -> bool {
match self {
Self::Inline(s) => s.contains(&format!("{} ", source)) || s.contains(&format!(" {}", source)),
Self::List(l) => l.contains(&source.into()),
}
}
/// Appends the given source to this directive.
pub fn push<S: AsRef<str>>(&mut self, source: S) {
match self {
Self::Inline(s) => {
s.push(' ');
s.push_str(source.as_ref());
}
Self::List(l) => {
l.push(source.as_ref().to_string());
}
}
}
/// Extends this CSP directive source list with the given array of sources.
pub fn extend(&mut self, sources: Vec<String>) {
for s in sources {
self.push(s);
}
}
}
/// A Content-Security-Policy definition.
/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", untagged)]
pub enum Csp {
/// The entire CSP policy in a single text string.
Policy(String),
/// An object mapping a directive with its sources values as a list of strings.
DirectiveMap(HashMap<String, CspDirectiveSources>),
}
impl From<HashMap<String, CspDirectiveSources>> for Csp {
fn from(map: HashMap<String, CspDirectiveSources>) -> Self {
Self::DirectiveMap(map)
}
}
impl From<Csp> for HashMap<String, CspDirectiveSources> {
fn from(csp: Csp) -> Self {
match csp {
Csp::Policy(policy) => {
let mut map = HashMap::new();
for directive in policy.split(';') {
let mut tokens = directive.trim().split(' ');
if let Some(directive) = tokens.next() {
let sources = tokens.map(|s| s.to_string()).collect::<Vec<String>>();
map.insert(directive.to_string(), CspDirectiveSources::List(sources));
}
}
map
}
Csp::DirectiveMap(m) => m,
}
}
}
impl Display for Csp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Policy(s) => write!(f, "{}", s),
Self::DirectiveMap(m) => {
let len = m.len();
let mut i = 0;
for (directive, sources) in m {
let sources: Vec<String> = sources.clone().into();
write!(f, "{} {}", directive, sources.join(" "))?;
i += 1;
if i != len {
write!(f, "; ")?;
}
}
Ok(())
}
}
}
}
/// Security configuration.
#[skip_serializing_none]
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
@ -604,12 +724,12 @@ pub struct SecurityConfig {
///
/// This is a really important part of the configuration since it helps you ensure your WebView is secured.
/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
pub csp: Option<String>,
pub csp: Option<Csp>,
/// The Content Security Policy that will be injected on all HTML files on development.
///
/// This is a really important part of the configuration since it helps you ensure your WebView is secured.
/// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.
pub dev_csp: Option<String>,
pub dev_csp: Option<Csp>,
/// Freeze the `Object.prototype` when using the custom protocol.
#[serde(default)]
pub freeze_prototype: bool,
@ -2399,10 +2519,49 @@ mod build {
}
}
impl ToTokens for CspDirectiveSources {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::config::CspDirectiveSources };
tokens.append_all(match self {
Self::Inline(sources) => {
let sources = sources.as_str();
quote!(#prefix::Inline(#sources.into()))
}
Self::List(list) => {
let list = vec_lit(list, str_lit);
quote!(#prefix::List(#list))
}
})
}
}
impl ToTokens for Csp {
fn to_tokens(&self, tokens: &mut TokenStream) {
let prefix = quote! { ::tauri::utils::config::Csp };
tokens.append_all(match self {
Self::Policy(policy) => {
let policy = policy.as_str();
quote!(#prefix::Policy(#policy.into()))
}
Self::DirectiveMap(list) => {
let map = map_lit(
quote! { ::std::collections::HashMap },
list,
str_lit,
identity,
);
quote!(#prefix::DirectiveMap(#map))
}
})
}
}
impl ToTokens for SecurityConfig {
fn to_tokens(&self, tokens: &mut TokenStream) {
let csp = opt_str_lit(self.csp.as_ref());
let dev_csp = opt_str_lit(self.dev_csp.as_ref());
let csp = opt_lit(self.csp.as_ref());
let dev_csp = opt_lit(self.dev_csp.as_ref());
let freeze_prototype = self.freeze_prototype;
literal_struct!(tokens, SecurityConfig, csp, dev_csp, freeze_prototype);

View File

@ -20,6 +20,7 @@ use tauri_macros::default_runtime;
use tauri_utils::pattern::isolation::RawIsolationPayload;
use tauri_utils::{
assets::{AssetKey, CspHash},
config::{Csp, CspDirectiveSources},
html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
};
@ -67,8 +68,8 @@ const MENU_EVENT: &str = "tauri://menu";
#[derive(Default)]
/// Spaced and quoted Content-Security-Policy hash values.
struct CspHashStrings {
script: String,
style: String,
script: Vec<String>,
style: Vec<String>,
}
/// Sets the CSP value to the asset HTML if needed (on Linux).
@ -78,20 +79,19 @@ fn set_csp<R: Runtime>(
assets: Arc<dyn Assets>,
asset_path: &AssetKey,
#[allow(unused_variables)] manager: &WindowManager<R>,
mut csp: String,
csp: Csp,
) -> String {
let mut csp = csp.into();
let hash_strings =
assets
.csp_hashes(asset_path)
.fold(CspHashStrings::default(), |mut acc, hash| {
match hash {
CspHash::Script(hash) => {
acc.script.push(' ');
acc.script.push_str(hash);
acc.script.push(hash.into());
}
CspHash::Style(hash) => {
acc.style.push(' ');
acc.style.push_str(hash);
acc.style.push(hash.into());
}
_csp_hash => {
#[cfg(debug_assertions)]
@ -120,15 +120,13 @@ fn set_csp<R: Runtime>(
#[cfg(feature = "isolation")]
if let Pattern::Isolation { schema, .. } = &manager.inner.pattern {
let default_src = format!("default-src {}", format_real_schema(schema));
if csp.contains("default-src") {
csp = csp.replace("default-src", &default_src);
} else {
csp.push_str("; ");
csp.push_str(&default_src);
}
let default_src = csp
.entry("default-src".into())
.or_insert_with(Default::default);
default_src.push(format_real_schema(schema));
}
let csp = Csp::DirectiveMap(csp).to_string();
#[cfg(target_os = "linux")]
{
*asset = asset.replacen(tauri_utils::html::CSP_TOKEN, &csp, 1);
@ -156,9 +154,9 @@ fn replace_with_callback<F: FnMut() -> String>(
fn replace_csp_nonce(
asset: &mut String,
token: &str,
csp: &mut String,
csp_attr: &str,
hashes: String,
csp: &mut HashMap<String, CspDirectiveSources>,
directive: &str,
hashes: Vec<String>,
) {
let mut nonces = Vec::new();
*asset = replace_with_callback(asset, token, || {
@ -168,29 +166,17 @@ fn replace_csp_nonce(
});
if !(nonces.is_empty() && hashes.is_empty()) {
let attr = format!(
"{} 'self'{}{}",
csp_attr,
if nonces.is_empty() {
"".into()
} else {
format!(
" {}",
nonces
.into_iter()
.map(|n| format!("'nonce-{}'", n))
.collect::<Vec<String>>()
.join(" ")
)
},
hashes
);
if csp.contains(csp_attr) {
*csp = csp.replace(csp_attr, &attr);
} else {
csp.push_str("; ");
csp.push_str(&attr);
let nonce_sources = nonces
.into_iter()
.map(|n| format!("'nonce-{}'", n))
.collect::<Vec<String>>();
let sources = csp.entry(directive.into()).or_insert_with(Default::default);
let self_source = "'self'".to_string();
if !sources.contains(&self_source) {
sources.push(self_source);
}
sources.extend(nonce_sources);
sources.extend(hashes);
}
}
@ -376,7 +362,7 @@ impl<R: Runtime> WindowManager<R> {
}
}
fn csp(&self) -> Option<String> {
fn csp(&self) -> Option<Csp> {
if cfg!(feature = "custom-protocol") {
self.inner.config.tauri.security.csp.clone()
} else {
@ -1045,7 +1031,7 @@ impl<R: Runtime> WindowManager<R> {
// naive way to check if it's an html
if html.contains('<') && html.contains('>') {
let mut document = tauri_utils::html::parse(html);
tauri_utils::html::inject_csp(&mut document, &csp);
tauri_utils::html::inject_csp(&mut document, &csp.to_string());
url.set_path(&format!("text/html,{}", document.to_string()));
}
}

View File

@ -122,7 +122,12 @@
}
],
"security": {
"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",
"csp": {
"default-src": "'self' customprotocol: asset:",
"font-src": ["https://fonts.gstatic.com"],
"img-src": "'self' asset: https://asset.localhost blob: data:",
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
},
"freezePrototype": true
},
"systemTray": {

View File

@ -867,6 +867,38 @@
},
"additionalProperties": false
},
"Csp": {
"description": "A Content-Security-Policy definition. See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.",
"anyOf": [
{
"description": "The entire CSP policy in a single text string.",
"type": "string"
},
{
"description": "An object mapping a directive with its sources values as a list of strings.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/CspDirectiveSources"
}
}
]
},
"CspDirectiveSources": {
"description": "A Content-Security-Policy directive source list. See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#sources>.",
"anyOf": [
{
"description": "An inline list of CSP sources. Same as [`Self::List`], but concatenated with a space separator.",
"type": "string"
},
{
"description": "A list of CSP sources. The collection will be concatenated with a space separator for the CSP string.",
"type": "array",
"items": {
"type": "string"
}
}
]
},
"DebConfig": {
"description": "Configuration for Debian (.deb) bundles.",
"type": "object",
@ -1310,16 +1342,24 @@
"properties": {
"csp": {
"description": "The Content Security Policy that will be injected on all HTML files on the built application. If [`dev_csp`](SecurityConfig.dev_csp) is not specified, this value is also injected on dev.\n\nThis is a really important part of the configuration since it helps you ensure your WebView is secured. See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/definitions/Csp"
},
{
"type": "null"
}
]
},
"devCsp": {
"description": "The Content Security Policy that will be injected on all HTML files on development.\n\nThis is a really important part of the configuration since it helps you ensure your WebView is secured. See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/definitions/Csp"
},
{
"type": "null"
}
]
},
"freezePrototype": {

View File

@ -729,7 +729,8 @@ pub fn command(_options: Options) -> Result<()> {
.tauri
.security
.csp
.clone()
.as_ref()
.map(|c| c.to_string())
.unwrap_or_else(|| "unset".to_string()),
)
.display();