Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): reintroduce CSP injection #1704

Merged
merged 1 commit into from
May 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/csp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"tauri-codegen": patch
"tauri-utils": patch
"tauri": patch
---

Reintroduce `csp` injection, configured on `tauri.conf.json > tauri > security > csp`.
8 changes: 6 additions & 2 deletions core/tauri-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
};
Expand Down
41 changes: 36 additions & 5 deletions core/tauri-codegen/src/embedded_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand All @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions core/tauri-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
24 changes: 23 additions & 1 deletion core/tauri-utils/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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 {
Expand All @@ -349,6 +360,7 @@ impl Default for TauriConfig {
cli: None,
bundle: BundleConfig::default(),
updater: UpdaterConfig::default(),
security: SecurityConfig::default(),
}
}
}
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -857,6 +878,7 @@ mod test {
pubkey: None,
endpoints: None,
},
security: SecurityConfig { csp: None },
};

// create a build config
Expand Down
71 changes: 71 additions & 0 deletions core/tauri-utils/src/html.rs
Original file line number Diff line number Diff line change
@@ -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
)
);
}
}
}
2 changes: 2 additions & 0 deletions core/tauri-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions examples/api/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
}
}
}
}
2 changes: 1 addition & 1 deletion examples/splashscreen/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tooling/cli.rs/templates/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
}
}
}