mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Adding Content-Security-Policy Headers and auto-generating integrity hashes (#740)
* Adding Content-Security-Policy Headers and auto-generating integrity hashes * created favicon and WASM loader as their own files * adding .map files from bootstrap
This commit is contained in:
parent
12852cf0a0
commit
d5fbb91a1c
|
@ -152,33 +152,27 @@ pub fn to_tide_response<T: Serialize>(
|
|||
// Handle the various end points we need to expose
|
||||
async fn index_view(_req: tide::Request<AppState>) -> tide::Result {
|
||||
let mut res = tide::Response::new(200);
|
||||
res.set_content_type("text/html;charset=utf-8");
|
||||
res.set_body(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Kanidm</title>
|
||||
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"/>
|
||||
<link rel="stylesheet" href="/pkg/style.css"/>
|
||||
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"></script>
|
||||
<script src="/pkg/external/confetti.js"></script>
|
||||
<script type="module" defer>
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
||||
</script>
|
||||
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🦀</text></svg>" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
res.set_content_type("text/html;charset=utf-8");
|
||||
res.set_body(r#"
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Kanidm</title>
|
||||
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"/>
|
||||
<link rel="stylesheet" href="/pkg/style.css"/>
|
||||
<script src="/pkg/external/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"></script>
|
||||
<script src="/pkg/external/confetti.js"></script>
|
||||
<script type="module" type="text/javascript" src="/pkg/wasmloader.js" integrity="sha384-==WASMHASH==">
|
||||
</script>
|
||||
|
||||
<link rel="icon" href="/pkg/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
);
|
||||
|
||||
Ok(res)
|
||||
|
@ -252,6 +246,49 @@ impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StrictRes
|
|||
Ok(response)
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct UIContentSecurityPolicyResponseMiddleware {
|
||||
// The sha384 hash of /pkg/wasmloader.js
|
||||
pub integrity_wasmloader: String,
|
||||
}
|
||||
|
||||
impl UIContentSecurityPolicyResponseMiddleware {
|
||||
fn new(integrity_wasmloader: String) -> Self {
|
||||
return Self {
|
||||
integrity_wasmloader,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State>
|
||||
for UIContentSecurityPolicyResponseMiddleware
|
||||
{
|
||||
async fn handle(
|
||||
&self,
|
||||
request: tide::Request<State>,
|
||||
next: tide::Next<'_, State>,
|
||||
) -> tide::Result {
|
||||
// This updates the UI body with the integrity hash value for the wasmloader.js file, and adds content-security-policy headers.
|
||||
|
||||
let mut response = next.run(request).await;
|
||||
|
||||
// grab the body we're intending to return at this point
|
||||
let body_str = response.take_body().into_string().await?;
|
||||
// update it with the hash
|
||||
response.set_body(body_str.replace("==WASMHASH==", self.integrity_wasmloader.as_str()));
|
||||
|
||||
response.insert_header(
|
||||
"content-security-policy",
|
||||
format!(
|
||||
"default-src https: self; img-src https: self; script-src https: 'sha384-{}' 'unsafe-eval' self;",
|
||||
self.integrity_wasmloader.as_str(),
|
||||
)
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
struct StrictRequestMiddleware;
|
||||
|
||||
|
@ -290,6 +327,32 @@ impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StrictReq
|
|||
}
|
||||
}
|
||||
|
||||
pub fn generate_integrity_hash(filename: String) -> Result<String, String> {
|
||||
let wasm_filepath = PathBuf::from(filename);
|
||||
match wasm_filepath.exists() {
|
||||
false => {
|
||||
return Err(format!(
|
||||
"Can't find {:?} to generate file hash",
|
||||
&wasm_filepath
|
||||
));
|
||||
}
|
||||
true => {
|
||||
let filecontents = match std::fs::read(&wasm_filepath) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"Failed to read {:?}, skipping: {:?}",
|
||||
wasm_filepath, error
|
||||
));
|
||||
}
|
||||
};
|
||||
let shasum =
|
||||
openssl::hash::hash(openssl::hash::MessageDigest::sha384(), &filecontents).unwrap();
|
||||
Ok(format!("{}", openssl::base64::encode_block(&shasum)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add request limits.
|
||||
pub fn create_https_server(
|
||||
address: String,
|
||||
|
@ -349,11 +412,18 @@ pub fn create_https_server(
|
|||
|
||||
let mut static_tserver = tserver.at("");
|
||||
static_tserver.with(StaticContentMiddleware::default());
|
||||
static_tserver.with(UIContentSecurityPolicyResponseMiddleware::new(
|
||||
generate_integrity_hash(env!("KANIDM_WEB_UI_PKG_PATH").to_owned() + "/wasmloader.js")
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
static_tserver.at("/").get(index_view);
|
||||
static_tserver.at("/ui/").get(index_view);
|
||||
static_tserver.at("/ui/*").get(index_view);
|
||||
static_tserver
|
||||
|
||||
let mut static_dir_tserver = tserver.at("");
|
||||
static_dir_tserver.with(StaticContentMiddleware::default());
|
||||
static_dir_tserver
|
||||
.at("/pkg")
|
||||
.serve_dir(env!("KANIDM_WEB_UI_PKG_PATH"))
|
||||
.map_err(|e| {
|
||||
|
@ -647,6 +717,9 @@ pub fn create_https_server(
|
|||
let tlsl = TlsListener::new(address, x_ref);
|
||||
*/
|
||||
|
||||
// adds Content-Security-Policy Headers when running in HTTPS
|
||||
// TODO: make this only apply to /ui/
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = tserver.listen(tlsl).await {
|
||||
error!(
|
||||
|
|
|
@ -10,7 +10,7 @@ use tokio::task;
|
|||
|
||||
pub const ADMIN_TEST_USER: &str = "admin";
|
||||
pub const ADMIN_TEST_PASSWORD: &str = "integration test admin password";
|
||||
static PORT_ALLOC: AtomicU16 = AtomicU16::new(18080);
|
||||
pub static PORT_ALLOC: AtomicU16 = AtomicU16::new(18080);
|
||||
|
||||
fn is_free_port(port: u16) -> bool {
|
||||
// TODO: Refactor to use `Result::is_err` in a future PR
|
||||
|
|
92
kanidmd/score/tests/https_middleware.rs
Normal file
92
kanidmd/score/tests/https_middleware.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use std::net::TcpStream;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
mod common;
|
||||
use crate::common::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER, PORT_ALLOC};
|
||||
|
||||
use kanidm::audit::LogLevel;
|
||||
use kanidm::config::{Configuration, IntegrationTestConfig, ServerRole};
|
||||
use kanidm::tracing_tree;
|
||||
use score::create_server_core;
|
||||
use tokio::task;
|
||||
|
||||
fn is_free_port(port: u16) -> bool {
|
||||
// TODO: Refactor to use `Result::is_err` in a future PR
|
||||
match TcpStream::connect(("0.0.0.0", port)) {
|
||||
Ok(_) => false,
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_https_middleware_headers() {
|
||||
// tests stuff
|
||||
let _ = tracing_tree::test_init();
|
||||
|
||||
let mut counter = 0;
|
||||
let port = loop {
|
||||
let possible_port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
|
||||
if is_free_port(possible_port) {
|
||||
break possible_port;
|
||||
}
|
||||
counter += 1;
|
||||
if counter >= 5 {
|
||||
eprintln!("Unable to allocate port!");
|
||||
assert!(false);
|
||||
}
|
||||
};
|
||||
|
||||
let int_config = Box::new(IntegrationTestConfig {
|
||||
admin_user: ADMIN_TEST_USER.to_string(),
|
||||
admin_password: ADMIN_TEST_PASSWORD.to_string(),
|
||||
});
|
||||
|
||||
// Setup the config ...
|
||||
let mut config = Configuration::new();
|
||||
config.address = format!("127.0.0.1:{}", port);
|
||||
config.secure_cookies = false;
|
||||
config.integration_test_config = Some(int_config);
|
||||
config.log_level = Some(LogLevel::Quiet as u32);
|
||||
config.role = ServerRole::WriteReplica;
|
||||
config.threads = 1;
|
||||
|
||||
create_server_core(config, false)
|
||||
.await
|
||||
.expect("failed to start server core");
|
||||
// We have to yield now to guarantee that the tide elements are setup.
|
||||
task::yield_now().await;
|
||||
|
||||
let addr = format!("http://127.0.0.1:{}/", port);
|
||||
|
||||
// here we test the /ui/ endpoint which should have the headers
|
||||
let response = match reqwest::get(format!("{}ui/", &addr)).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
panic!("Failed to query {:?} : {:#?}", addr, error);
|
||||
}
|
||||
};
|
||||
eprintln!("response: {:#?}", response);
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
eprintln!(
|
||||
"csp headers: {:#?}",
|
||||
response.headers().get("content-security-policy")
|
||||
);
|
||||
assert_ne!(response.headers().get("content-security-policy"), None);
|
||||
|
||||
// here we test the /pkg/ endpoint which shouldn't have the headers
|
||||
let response =
|
||||
match reqwest::get(format!("{}pkg/external/bootstrap.bundle.min.js", &addr)).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
panic!("Failed to query {:?} : {:#?}", addr, error);
|
||||
}
|
||||
};
|
||||
eprintln!("response: {:#?}", response);
|
||||
assert_eq!(response.status(), 200);
|
||||
eprintln!(
|
||||
"csp headers: {:#?}",
|
||||
response.headers().get("content-security-policy")
|
||||
);
|
||||
assert_eq!(response.headers().get("content-security-policy"), None);
|
||||
}
|
1
kanidmd_web_ui/pkg/external/bootstrap.bundle.min.js.map
vendored
Normal file
1
kanidmd_web_ui/pkg/external/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
kanidmd_web_ui/pkg/external/bootstrap.min.css.map
vendored
Normal file
1
kanidmd_web_ui/pkg/external/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
kanidmd_web_ui/pkg/favicon.svg
Normal file
3
kanidmd_web_ui/pkg/favicon.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<text y=".9em" font-size="90">🦀</text>
|
||||
</svg>
|
After Width: | Height: | Size: 111 B |
7
kanidmd_web_ui/pkg/wasmloader.js
Normal file
7
kanidmd_web_ui/pkg/wasmloader.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// loads the module which loads the WASM. It's loaders all the way down.
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
Loading…
Reference in a new issue