feat: better security with <script nonce="">
This commit is contained in:
parent
3de209ec2e
commit
88a6807b8f
File diff suppressed because it is too large
Load Diff
|
@ -36,6 +36,7 @@ url = "2.3.1"
|
||||||
actix-governor = "0.5.0"
|
actix-governor = "0.5.0"
|
||||||
governor = "0.6.3"
|
governor = "0.6.3"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
actix-web-lab = "0.20.2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = "symbols"
|
strip = "symbols"
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
<br />
|
<br />
|
||||||
<input id="password" name="password" type="password" />
|
<input id="password" name="password" type="password" />
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/auth-hide.js"></script>
|
<script nonce="{script_nonce}" src="/static/auth-hide.js"></script>
|
||||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -6,6 +6,7 @@ mod file_info;
|
||||||
mod mime_relations;
|
mod mime_relations;
|
||||||
mod multipart;
|
mod multipart;
|
||||||
mod rate_limit;
|
mod rate_limit;
|
||||||
|
mod script_nonce;
|
||||||
mod template;
|
mod template;
|
||||||
mod upload;
|
mod upload;
|
||||||
|
|
||||||
|
@ -14,22 +15,19 @@ use actix_files::Files;
|
||||||
use actix_governor::{Governor, GovernorConfigBuilder};
|
use actix_governor::{Governor, GovernorConfigBuilder};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
http::header::{
|
http::header::{
|
||||||
HeaderName, CONTENT_SECURITY_POLICY, PERMISSIONS_POLICY, REFERRER_POLICY,
|
HeaderName, PERMISSIONS_POLICY, REFERRER_POLICY, X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
|
||||||
X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS, X_XSS_PROTECTION,
|
X_XSS_PROTECTION,
|
||||||
},
|
},
|
||||||
middleware::{self, Condition, DefaultHeaders},
|
middleware::{self, Condition, DefaultHeaders},
|
||||||
web::{self, Data},
|
web::{self, Data},
|
||||||
App, Error, HttpResponse, HttpServer,
|
App, Error, HttpResponse, HttpServer,
|
||||||
};
|
};
|
||||||
|
use actix_web_lab::middleware::from_fn;
|
||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
use sqlx::postgres::PgPool;
|
use sqlx::postgres::PgPool;
|
||||||
use std::env;
|
use std::env;
|
||||||
use tokio::sync::mpsc::channel;
|
use tokio::sync::mpsc::channel;
|
||||||
|
|
||||||
const DEFAULT_CONTENT_SECURITY_POLICY: (HeaderName, &str) = (
|
|
||||||
CONTENT_SECURITY_POLICY,
|
|
||||||
"default-src 'none'; connect-src 'self'; img-src 'self'; media-src 'self'; font-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'self';"
|
|
||||||
);
|
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
const DEFAULT_PERMISSIONS: (HeaderName, &str) = (
|
const DEFAULT_PERMISSIONS: (HeaderName, &str) = (
|
||||||
PERMISSIONS_POLICY,
|
PERMISSIONS_POLICY,
|
||||||
|
@ -85,7 +83,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(
|
.wrap(
|
||||||
DefaultHeaders::new()
|
DefaultHeaders::new()
|
||||||
.add(DEFAULT_CONTENT_SECURITY_POLICY)
|
|
||||||
.add(DEFAULT_PERMISSIONS)
|
.add(DEFAULT_PERMISSIONS)
|
||||||
.add(DEFAULT_CONTENT_TYPE_OPTIONS)
|
.add(DEFAULT_CONTENT_TYPE_OPTIONS)
|
||||||
.add(DEFAULT_FRAME_OPTIONS)
|
.add(DEFAULT_FRAME_OPTIONS)
|
||||||
|
@ -94,6 +91,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
)
|
)
|
||||||
.wrap(middleware::Compress::default())
|
.wrap(middleware::Compress::default())
|
||||||
.wrap(middleware::NormalizePath::trim())
|
.wrap(middleware::NormalizePath::trim())
|
||||||
|
.wrap(from_fn(script_nonce::insert_script_nonce))
|
||||||
.app_data(db.clone())
|
.app_data(db.clone())
|
||||||
.app_data(expiry_watch_sender.clone())
|
.app_data(expiry_watch_sender.clone())
|
||||||
.app_data(config.clone())
|
.app_data(config.clone())
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
use actix_web::{
|
||||||
|
body::MessageBody,
|
||||||
|
dev::{ServiceRequest, ServiceResponse},
|
||||||
|
http::header::{HeaderValue, CONTENT_SECURITY_POLICY},
|
||||||
|
Error, HttpMessage,
|
||||||
|
};
|
||||||
|
use actix_web_lab::middleware::Next;
|
||||||
|
use rand::Rng;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
pub struct ScriptNonce(String);
|
||||||
|
|
||||||
|
impl Display for ScriptNonce {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_script_nonce(
|
||||||
|
req: ServiceRequest,
|
||||||
|
next: Next<impl MessageBody>,
|
||||||
|
) -> Result<ServiceResponse<impl MessageBody>, Error> {
|
||||||
|
let script_nonce = format!("{:02x}", rand::thread_rng().gen::<u128>());
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(ScriptNonce(script_nonce.clone()));
|
||||||
|
let mut res = next.call(req).await;
|
||||||
|
if let Ok(res) = res.as_mut() {
|
||||||
|
let value = format!("default-src 'none'; connect-src 'self'; img-src 'self'; media-src 'self'; font-src 'self'; script-src 'nonce-{script_nonce}'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'self'; require-trusted-types-for 'script';");
|
||||||
|
res.headers_mut().insert(
|
||||||
|
CONTENT_SECURITY_POLICY,
|
||||||
|
HeaderValue::from_str(&value).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
use std::{cmp, io::ErrorKind, str::FromStr};
|
use std::{cmp, io::ErrorKind, str::FromStr};
|
||||||
|
|
||||||
use actix_web::HttpRequest;
|
use actix_web::{HttpMessage, HttpRequest};
|
||||||
use time::Duration;
|
use time::Duration;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::{config::Config, script_nonce::ScriptNonce};
|
||||||
|
|
||||||
const INDEX_HTML: &str = include_str!("../template/index.html");
|
const INDEX_HTML: &str = include_str!("../template/index.html");
|
||||||
const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js");
|
const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js");
|
||||||
|
@ -34,7 +34,8 @@ pub fn build_uploaded_html(
|
||||||
} else {
|
} else {
|
||||||
UPLOAD_HTML.replace("{link}", &get_file_url(req, id, name))
|
UPLOAD_HTML.replace("{link}", &get_file_url(req, id, name))
|
||||||
};
|
};
|
||||||
insert_abuse_template(upload_html, None, config)
|
let upload_html = insert_abuse_template(upload_html, None, config);
|
||||||
|
insert_script_nonce(req, upload_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
|
pub fn get_file_url(req: &HttpRequest, id: &str, name: Option<&str>) -> String {
|
||||||
|
@ -68,15 +69,11 @@ pub fn build_html_view_template(
|
||||||
.replace("{file_name}", &name_snippet)
|
.replace("{file_name}", &name_snippet)
|
||||||
.replace("{text}", &encoded_content)
|
.replace("{text}", &encoded_content)
|
||||||
};
|
};
|
||||||
insert_abuse_template(html, Some(req), config)
|
let html = insert_abuse_template(html, Some(req), config);
|
||||||
|
insert_script_nonce(req, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn write_prefillable_templates(config: &Config) {
|
pub async fn write_prefillable_templates(config: &Config) {
|
||||||
let index_path = config.static_dir.join("index.html");
|
|
||||||
fs::write(index_path, build_index_html(config))
|
|
||||||
.await
|
|
||||||
.expect("could not write index.html to static folder");
|
|
||||||
|
|
||||||
let auth_hide_path = config.static_dir.join("auth-hide.js");
|
let auth_hide_path = config.static_dir.join("auth-hide.js");
|
||||||
if let Some(auth_hide_js) = build_auth_hide_js(config) {
|
if let Some(auth_hide_js) = build_auth_hide_js(config) {
|
||||||
fs::write(auth_hide_path, auth_hide_js)
|
fs::write(auth_hide_path, auth_hide_js)
|
||||||
|
@ -90,7 +87,7 @@ pub async fn write_prefillable_templates(config: &Config) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_index_html(config: &Config) -> String {
|
pub fn build_index_html(req: &HttpRequest, config: &Config) -> String {
|
||||||
let mut html = INDEX_HTML.to_owned();
|
let mut html = INDEX_HTML.to_owned();
|
||||||
if let Some(limit) = config.no_auth_limits.as_ref() {
|
if let Some(limit) = config.no_auth_limits.as_ref() {
|
||||||
html = html
|
html = html
|
||||||
|
@ -115,7 +112,7 @@ fn build_index_html(config: &Config) -> String {
|
||||||
} else {
|
} else {
|
||||||
html = html.replace("{max_size_snippet}", "");
|
html = html.replace("{max_size_snippet}", "");
|
||||||
};
|
};
|
||||||
html
|
insert_script_nonce(req, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_abuse_template(html: String, req: Option<&HttpRequest>, config: &Config) -> String {
|
pub fn insert_abuse_template(html: String, req: Option<&HttpRequest>, config: &Config) -> String {
|
||||||
|
@ -134,6 +131,12 @@ pub fn insert_abuse_template(html: String, req: Option<&HttpRequest>, config: &C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_script_nonce(req: &HttpRequest, html: String) -> String {
|
||||||
|
let extensions = &req.extensions();
|
||||||
|
let script_nonce = extensions.get::<ScriptNonce>().expect("script_nonce available");
|
||||||
|
html.replace("{script_nonce}", &script_nonce.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn render_file_size(size: u64) -> String {
|
fn render_file_size(size: u64) -> String {
|
||||||
let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5);
|
let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5);
|
||||||
let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize];
|
let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize];
|
||||||
|
|
|
@ -3,7 +3,6 @@ use std::io::ErrorKind;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::file_info::FileInfo;
|
use crate::file_info::FileInfo;
|
||||||
use crate::{file_info, multipart, template};
|
use crate::{file_info, multipart, template};
|
||||||
use actix_files::NamedFile;
|
|
||||||
use actix_multipart::Multipart;
|
use actix_multipart::Multipart;
|
||||||
use actix_web::http::header::LOCATION;
|
use actix_web::http::header::LOCATION;
|
||||||
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
|
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
|
||||||
|
@ -18,12 +17,11 @@ const ID_CHARS: &[char] = &[
|
||||||
'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
];
|
];
|
||||||
|
|
||||||
pub async fn index(config: web::Data<Config>) -> Result<NamedFile, Error> {
|
pub async fn index(req: HttpRequest, config: web::Data<Config>) -> HttpResponse {
|
||||||
let file = NamedFile::open(config.static_dir.join("index.html")).map_err(|file_err| {
|
let index_html = template::build_index_html(&req, &config);
|
||||||
log::error!("index.html could not be read {:?}", file_err);
|
HttpResponse::Ok()
|
||||||
error::ErrorInternalServerError("this file should be here but could not be found")
|
.content_type("text/html")
|
||||||
})?;
|
.body(index_html)
|
||||||
Ok(file.disable_content_disposition())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upload(
|
pub async fn upload(
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
repo
|
repo
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/static/paste.js"></script>
|
<script nonce="{script_nonce}" src="/static/paste.js"></script>
|
||||||
<script src="/static/origin.js"></script>
|
<script nonce="{script_nonce}" src="/static/origin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -36,6 +36,6 @@
|
||||||
repo
|
repo
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/static/copy.js"></script>
|
<script nonce="{script_nonce}" src="/static/copy.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -36,6 +36,6 @@
|
||||||
repo
|
repo
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/static/copy.js"></script>
|
<script nonce="{script_nonce}" src="/static/copy.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -32,6 +32,6 @@
|
||||||
repo
|
repo
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/static/copy.js"></script>
|
<script nonce="{script_nonce}" src="/static/copy.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -44,6 +44,6 @@
|
||||||
repo
|
repo
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/static/copy.js"></script>
|
<script nonce="{script_nonce}" src="/static/copy.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue