feat: better security with <script nonce="">
This commit is contained in:
parent
3de209ec2e
commit
bb35dd97a2
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
34
src/main.rs
34
src/main.rs
|
@ -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())
|
||||
|
|
|
@ -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 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];
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -36,6 +36,6 @@
|
|||
repo
|
||||
</a>
|
||||
</footer>
|
||||
<script src="/static/copy.js"></script>
|
||||
<script nonce="{script_nonce}" src="/static/copy.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -36,6 +36,6 @@
|
|||
repo
|
||||
</a>
|
||||
</footer>
|
||||
<script src="/static/copy.js"></script>
|
||||
<script nonce="{script_nonce}" src="/static/copy.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -32,6 +32,6 @@
|
|||
repo
|
||||
</a>
|
||||
</footer>
|
||||
<script src="/static/copy.js"></script>
|
||||
<script nonce="{script_nonce}" src="/static/copy.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -44,6 +44,6 @@
|
|||
repo
|
||||
</a>
|
||||
</footer>
|
||||
<script src="/static/copy.js"></script>
|
||||
<script nonce="{script_nonce}" src="/static/copy.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue