mirror of https://github.com/tauri-apps/tauri
parent
141133a414
commit
3fe0260f4c
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Allows the configuration CSP to be an object mapping a directive name to its source list.
|
|
@ -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);
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue