feat: better security with <script nonce="">

This commit is contained in:
neri 2024-07-26 02:09:34 +02:00
parent 3de209ec2e
commit 88a6807b8f
12 changed files with 375 additions and 195 deletions

471
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@ url = "2.3.1"
actix-governor = "0.5.0"
governor = "0.6.3"
lazy_static = "1.4.0"
actix-web-lab = "0.20.2"
[profile.release]
strip = "symbols"

View File

@ -6,4 +6,4 @@
<br />
<input id="password" name="password" type="password" />
</div>
<script src="/static/auth-hide.js"></script>
<script nonce="{script_nonce}" src="/static/auth-hide.js"></script>

View File

@ -6,6 +6,7 @@ mod file_info;
mod mime_relations;
mod multipart;
mod rate_limit;
mod script_nonce;
mod template;
mod upload;
@ -14,22 +15,19 @@ use actix_files::Files;
use actix_governor::{Governor, GovernorConfigBuilder};
use actix_web::{
http::header::{
HeaderName, CONTENT_SECURITY_POLICY, PERMISSIONS_POLICY, REFERRER_POLICY,
X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS, X_XSS_PROTECTION,
HeaderName, PERMISSIONS_POLICY, REFERRER_POLICY, X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
X_XSS_PROTECTION,
},
middleware::{self, Condition, DefaultHeaders},
web::{self, Data},
App, Error, HttpResponse, HttpServer,
};
use actix_web_lab::middleware::from_fn;
use env_logger::Env;
use sqlx::postgres::PgPool;
use std::env;
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)]
const DEFAULT_PERMISSIONS: (HeaderName, &str) = (
PERMISSIONS_POLICY,
@ -85,7 +83,6 @@ async fn main() -> std::io::Result<()> {
App::new()
.wrap(
DefaultHeaders::new()
.add(DEFAULT_CONTENT_SECURITY_POLICY)
.add(DEFAULT_PERMISSIONS)
.add(DEFAULT_CONTENT_TYPE_OPTIONS)
.add(DEFAULT_FRAME_OPTIONS)
@ -94,6 +91,7 @@ async fn main() -> std::io::Result<()> {
)
.wrap(middleware::Compress::default())
.wrap(middleware::NormalizePath::trim())
.wrap(from_fn(script_nonce::insert_script_nonce))
.app_data(db.clone())
.app_data(expiry_watch_sender.clone())
.app_data(config.clone())

35
src/script_nonce.rs Normal file
View File

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

View File

@ -1,11 +1,11 @@
use std::{cmp, io::ErrorKind, str::FromStr};
use actix_web::HttpRequest;
use actix_web::{HttpMessage, HttpRequest};
use time::Duration;
use tokio::fs;
use url::Url;
use crate::config::Config;
use crate::{config::Config, script_nonce::ScriptNonce};
const INDEX_HTML: &str = include_str!("../template/index.html");
const AUTH_HIDE_JS: &str = include_str!("../template/auth-hide.js");
@ -34,7 +34,8 @@ pub fn build_uploaded_html(
} else {
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 {
@ -68,15 +69,11 @@ pub fn build_html_view_template(
.replace("{file_name}", &name_snippet)
.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) {
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");
if let Some(auth_hide_js) = build_auth_hide_js(config) {
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();
if let Some(limit) = config.no_auth_limits.as_ref() {
html = html
@ -115,7 +112,7 @@ fn build_index_html(config: &Config) -> String {
} else {
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 {
@ -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 {
let magnitude = cmp::min((size as f64).log(1024.0) as u32, 5);
let prefix = ["", "ki", "Mi", "Gi", "Ti", "Pi"][magnitude as usize];

View File

@ -3,7 +3,6 @@ use std::io::ErrorKind;
use crate::config::Config;
use crate::file_info::FileInfo;
use crate::{file_info, multipart, template};
use actix_files::NamedFile;
use actix_multipart::Multipart;
use actix_web::http::header::LOCATION;
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',
];
pub async fn index(config: web::Data<Config>) -> Result<NamedFile, Error> {
let file = NamedFile::open(config.static_dir.join("index.html")).map_err(|file_err| {
log::error!("index.html could not be read {:?}", file_err);
error::ErrorInternalServerError("this file should be here but could not be found")
})?;
Ok(file.disable_content_disposition())
pub async fn index(req: HttpRequest, config: web::Data<Config>) -> HttpResponse {
let index_html = template::build_index_html(&req, &config);
HttpResponse::Ok()
.content_type("text/html")
.body(index_html)
}
pub async fn upload(

View File

@ -93,7 +93,7 @@
repo
</a>
</footer>
<script src="/static/paste.js"></script>
<script src="/static/origin.js"></script>
<script nonce="{script_nonce}" src="/static/paste.js"></script>
<script nonce="{script_nonce}" src="/static/origin.js"></script>
</body>
</html>

View File

@ -36,6 +36,6 @@
repo
</a>
</footer>
<script src="/static/copy.js"></script>
<script nonce="{script_nonce}" src="/static/copy.js"></script>
</body>
</html>

View File

@ -36,6 +36,6 @@
repo
</a>
</footer>
<script src="/static/copy.js"></script>
<script nonce="{script_nonce}" src="/static/copy.js"></script>
</body>
</html>

View File

@ -32,6 +32,6 @@
repo
</a>
</footer>
<script src="/static/copy.js"></script>
<script nonce="{script_nonce}" src="/static/copy.js"></script>
</body>
</html>

View File

@ -44,6 +44,6 @@
repo
</a>
</footer>
<script src="/static/copy.js"></script>
<script nonce="{script_nonce}" src="/static/copy.js"></script>
</body>
</html>