mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-01 10:01:07 +02:00
feat(core): reintroduce CSP injection (#1704)
This commit is contained in:
committed by
GitHub
parent
428d50add4
commit
6132f3f4fe
7
.changes/csp.md
Normal file
7
.changes/csp.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"tauri-codegen": patch
|
||||
"tauri-utils": patch
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Reintroduce `csp` injection, configured on `tauri.conf.json > tauri > security > csp`.
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::embedded_assets::{EmbeddedAssets, EmbeddedAssetsError};
|
||||
use crate::embedded_assets::{AssetOptions, EmbeddedAssets, EmbeddedAssetsError};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use std::path::PathBuf;
|
||||
@@ -37,7 +37,11 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
|
||||
|
||||
// generate the assets inside the dist dir into a perfect hash function
|
||||
let assets = if let Some(assets_path) = assets_path {
|
||||
EmbeddedAssets::new(&assets_path)?
|
||||
let mut options = AssetOptions::new();
|
||||
if let Some(csp) = &config.tauri.security.csp {
|
||||
options = options.csp(csp.clone());
|
||||
}
|
||||
EmbeddedAssets::new(&assets_path, options)?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
@@ -6,10 +6,11 @@ use proc_macro2::TokenStream;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::OsStr,
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tauri_utils::assets::AssetKey;
|
||||
use tauri_utils::{assets::AssetKey, html::inject_csp};
|
||||
use thiserror::Error;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
@@ -62,9 +63,28 @@ pub enum EmbeddedAssetsError {
|
||||
#[derive(Default)]
|
||||
pub struct EmbeddedAssets(HashMap<AssetKey, (PathBuf, PathBuf)>);
|
||||
|
||||
/// Options used to embed assets.
|
||||
#[derive(Default)]
|
||||
pub struct AssetOptions {
|
||||
csp: Option<String>,
|
||||
}
|
||||
|
||||
impl AssetOptions {
|
||||
/// Creates the default asset options.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the content security policy to add to HTML files.
|
||||
pub fn csp(mut self, csp: String) -> Self {
|
||||
self.csp.replace(csp);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EmbeddedAssets {
|
||||
/// Compress a directory of assets, ready to be generated into a [`tauri_utils::assets::Assets`].
|
||||
pub fn new(path: &Path) -> Result<Self, EmbeddedAssetsError> {
|
||||
pub fn new(path: &Path, options: AssetOptions) -> Result<Self, EmbeddedAssetsError> {
|
||||
WalkDir::new(&path)
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
@@ -73,7 +93,7 @@ impl EmbeddedAssets {
|
||||
Ok(entry) if entry.file_type().is_dir() => None,
|
||||
|
||||
// compress all files encountered
|
||||
Ok(entry) => Some(Self::compress_file(path, entry.path())),
|
||||
Ok(entry) => Some(Self::compress_file(path, entry.path(), &options)),
|
||||
|
||||
// pass down error through filter to fail when encountering any error
|
||||
Err(error) => Some(Err(EmbeddedAssetsError::Walkdir {
|
||||
@@ -96,11 +116,22 @@ impl EmbeddedAssets {
|
||||
}
|
||||
|
||||
/// Compress a file and spit out the information in a [`HashMap`] friendly form.
|
||||
fn compress_file(prefix: &Path, path: &Path) -> Result<Asset, EmbeddedAssetsError> {
|
||||
let input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
|
||||
fn compress_file(
|
||||
prefix: &Path,
|
||||
path: &Path,
|
||||
options: &AssetOptions,
|
||||
) -> Result<Asset, EmbeddedAssetsError> {
|
||||
let mut input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead {
|
||||
path: path.to_owned(),
|
||||
error,
|
||||
})?;
|
||||
if let Some(csp) = &options.csp {
|
||||
if path.extension() == Some(OsStr::new("html")) {
|
||||
input = inject_csp(String::from_utf8_lossy(&input).into_owned(), csp)
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
}
|
||||
}
|
||||
|
||||
// we must canonicalize the base of our paths to allow long paths on windows
|
||||
let out_dir = std::env::var("OUT_DIR")
|
||||
|
||||
@@ -16,6 +16,8 @@ thiserror = "1.0.24"
|
||||
phf = { version = "0.8", features = [ "macros" ] }
|
||||
zstd = "0.7"
|
||||
url = { version = "2.2", features = [ "serde" ] }
|
||||
kuchiki = "0.8"
|
||||
html5ever = "0.25"
|
||||
proc-macro2 = { version = "1.0", optional = true }
|
||||
quote = { version = "1.0", optional = true }
|
||||
|
||||
|
||||
@@ -164,6 +164,14 @@ impl Default for UpdaterConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Security configuration.
|
||||
#[derive(PartialEq, Deserialize, Debug, Clone, Default)]
|
||||
#[serde(tag = "updater", rename_all = "camelCase")]
|
||||
pub struct SecurityConfig {
|
||||
/// Content security policy to inject to HTML files with the custom protocol.
|
||||
pub csp: Option<String>,
|
||||
}
|
||||
|
||||
/// A CLI argument definition
|
||||
#[derive(PartialEq, Deserialize, Debug, Default, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -340,6 +348,9 @@ pub struct TauriConfig {
|
||||
/// The updater configuration.
|
||||
#[serde(default)]
|
||||
pub updater: UpdaterConfig,
|
||||
/// The security configuration.
|
||||
#[serde(default)]
|
||||
pub security: SecurityConfig,
|
||||
}
|
||||
|
||||
impl Default for TauriConfig {
|
||||
@@ -349,6 +360,7 @@ impl Default for TauriConfig {
|
||||
cli: None,
|
||||
bundle: BundleConfig::default(),
|
||||
updater: UpdaterConfig::default(),
|
||||
security: SecurityConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -756,14 +768,23 @@ mod build {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for SecurityConfig {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let csp = opt_str_lit(self.csp.as_ref());
|
||||
|
||||
literal_struct!(tokens, SecurityConfig, csp);
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for TauriConfig {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let windows = vec_lit(&self.windows, identity);
|
||||
let cli = opt_lit(self.cli.as_ref());
|
||||
let bundle = &self.bundle;
|
||||
let updater = &self.updater;
|
||||
let security = &self.security;
|
||||
|
||||
literal_struct!(tokens, TauriConfig, windows, cli, bundle, updater);
|
||||
literal_struct!(tokens, TauriConfig, windows, cli, bundle, updater, security);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,6 +878,7 @@ mod test {
|
||||
pubkey: None,
|
||||
endpoints: None,
|
||||
},
|
||||
security: SecurityConfig { csp: None },
|
||||
};
|
||||
|
||||
// create a build config
|
||||
|
||||
71
core/tauri-utils/src/html.rs
Normal file
71
core/tauri-utils/src/html.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use html5ever::{
|
||||
interface::QualName,
|
||||
namespace_url, ns,
|
||||
tendril::{fmt::UTF8, NonAtomic, Tendril},
|
||||
LocalName,
|
||||
};
|
||||
use kuchiki::{traits::*, Attribute, ExpandedName, NodeRef};
|
||||
|
||||
/// Injects a content security policy to the HTML.
|
||||
pub fn inject_csp<H: Into<Tendril<UTF8, NonAtomic>>>(html: H, csp: &str) -> String {
|
||||
let document = kuchiki::parse_html().one(html);
|
||||
if let Ok(ref head) = document.select_first("head") {
|
||||
head.as_node().append(create_csp_meta_tag(csp));
|
||||
} else {
|
||||
let head = NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), LocalName::from("head")),
|
||||
None,
|
||||
);
|
||||
head.append(create_csp_meta_tag(csp));
|
||||
document.prepend(head);
|
||||
}
|
||||
document.to_string()
|
||||
}
|
||||
|
||||
fn create_csp_meta_tag(csp: &str) -> NodeRef {
|
||||
NodeRef::new_element(
|
||||
QualName::new(None, ns!(html), LocalName::from("meta")),
|
||||
vec![
|
||||
(
|
||||
ExpandedName::new(ns!(), LocalName::from("http-equiv")),
|
||||
Attribute {
|
||||
prefix: None,
|
||||
value: "Content-Security-Policy".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
ExpandedName::new(ns!(), LocalName::from("content")),
|
||||
Attribute {
|
||||
prefix: None,
|
||||
value: csp.into(),
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn csp() {
|
||||
let htmls = vec![
|
||||
"<html><head></head></html>".to_string(),
|
||||
"<html></html>".to_string(),
|
||||
];
|
||||
for html in htmls {
|
||||
let csp = "default-src 'self'; img-src https://*; child-src 'none';";
|
||||
let new = super::inject_csp(html, csp);
|
||||
assert_eq!(
|
||||
new,
|
||||
format!(
|
||||
r#"<html><head><meta content="{}" http-equiv="Content-Security-Policy"></head><body></body></html>"#,
|
||||
csp
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
pub mod assets;
|
||||
/// Tauri config definition.
|
||||
pub mod config;
|
||||
/// Tauri HTML processing.
|
||||
pub mod html;
|
||||
/// Platform helpers
|
||||
pub mod platform;
|
||||
/// Process helpers
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline' img-src: 'self'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline' img-src: 'self'"
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
|
||||
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline' img-src: 'self'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user