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

This commit is contained in:
neri 2024-07-26 02:09:34 +02:00
parent 3de209ec2e
commit bb35dd97a2
12 changed files with 387 additions and 205 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,31 +15,29 @@ use actix_files::Files;
use actix_governor::{Governor, GovernorConfigBuilder};
use actix_web::{
http::header::{
HeaderName, CONTENT_SECURITY_POLICY, PERMISSIONS_POLICY, REFERRER_POLICY,
HeaderName, CROSS_ORIGIN_OPENER_POLICY, 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) = (
static DEFAULT_PERMISSIONS: (HeaderName, &str) = (
PERMISSIONS_POLICY,
"accelerometer=(), ambient-light-sensor=(), battery=(), camera=(), display-capture=(), document-domain=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=(), usb=(), web-share=()"
);
const DEFAULT_CONTENT_TYPE_OPTIONS: (HeaderName, &str) = (X_CONTENT_TYPE_OPTIONS, "nosniff");
const DEFAULT_FRAME_OPTIONS: (HeaderName, &str) = (X_FRAME_OPTIONS, "deny");
const DEFAULT_XSS_PROTECTION: (HeaderName, &str) = (X_XSS_PROTECTION, "1; mode=block");
const DEFAULT_REFERRER_POLICY: (HeaderName, &str) = (REFERRER_POLICY, "no-referrer");
static DEFAULT_CONTENT_TYPE_OPTIONS: (HeaderName, &str) = (X_CONTENT_TYPE_OPTIONS, "nosniff");
static DEFAULT_FRAME_OPTIONS: (HeaderName, &str) = (X_FRAME_OPTIONS, "deny");
static DEFAULT_XSS_PROTECTION: (HeaderName, &str) = (X_XSS_PROTECTION, "1; mode=block");
static DEFAULT_REFERRER_POLICY: (HeaderName, &str) = (REFERRER_POLICY, "no-referrer");
static DEFAULT_CROSS_ORIGIN_OPENER_POLICY: (HeaderName, &str) =
(CROSS_ORIGIN_OPENER_POLICY, "same-origin");
async fn not_found() -> Result<HttpResponse, Error> {
Ok(HttpResponse::NotFound()
@ -85,15 +84,16 @@ 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)
.add(DEFAULT_XSS_PROTECTION)
.add(DEFAULT_REFERRER_POLICY),
.add(DEFAULT_PERMISSIONS.clone())
.add(DEFAULT_CONTENT_TYPE_OPTIONS.clone())
.add(DEFAULT_FRAME_OPTIONS.clone())
.add(DEFAULT_XSS_PROTECTION.clone())
.add(DEFAULT_REFERRER_POLICY.clone())
.add(DEFAULT_CROSS_ORIGIN_OPENER_POLICY.clone()),
)
.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>