feat(core): reintroduce CSP injection (#1704)

This commit is contained in:
Lucas Fernandes Nogueira
2021-05-04 23:31:05 -03:00
committed by GitHub
parent 428d50add4
commit 6132f3f4fe
10 changed files with 151 additions and 12 deletions

7
.changes/csp.md Normal file
View File

@@ -0,0 +1,7 @@
---
"tauri-codegen": patch
"tauri-utils": patch
"tauri": patch
---
Reintroduce `csp` injection, configured on `tauri.conf.json > tauri > security > csp`.

View File

@@ -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()
};

View File

@@ -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")

View File

@@ -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 }

View File

@@ -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

View 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
)
);
}
}
}

View File

@@ -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

View File

@@ -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'"
}
}
}
}

View File

@@ -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

View File

@@ -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'"
}
}
}