Commit 96cbceb9 authored by Konrad Borowski's avatar Konrad Borowski

Merge branch 'csp' into 'master'

Use CSP script-src: 'strict-dynamic'

See merge request pastebin.run/server!61
parents a5ee95e8 4b88d280
Pipeline #91311068 passed with stage
in 22 minutes and 5 seconds
......@@ -1178,6 +1178,7 @@ name = "pastebinrun"
version = "0.1.0"
dependencies = [
"ammonia 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
......
......@@ -8,6 +8,7 @@ build = "src/build.rs"
[dependencies]
ammonia = "3.0.0"
base64 = "0.10.1"
chrono = "0.4.6"
diesel = { version = "1.4.1", features = ["chrono", "postgres", "r2d2"] }
diesel_migrations = "1.4.0"
......
......@@ -2,6 +2,9 @@ import addOptionsLink from './views/config-link'
import createSettings from './views/config-page/config-page'
import createEditor from './views/editor/editor'
declare var __webpack_nonce__: string
__webpack_nonce__ = document.getElementsByTagName('link')[0].getAttribute('nonce')
addOptionsLink()
const convertedNodes = [
......
......@@ -20,6 +20,7 @@ fn main() -> Result<(), Box<dyn Error>> {
for file in WalkDir::new("js") {
println!("cargo:rerun-if-changed={}", file?.path().display());
}
println!("cargo:rerun-if-changed=webpack.config.js");
run_command("npm install", "Installing npm modules failed");
run_command("node_modules/.bin/webpack", "Webpack failed");
println!(
......
pub mod language;
pub mod paste;
pub mod session;
use crate::Connection;
use warp::http::header::CONTENT_SECURITY_POLICY;
use warp::http::response::{Builder, Response};
pub struct Session {
pub nonce: String,
pub connection: Connection,
}
impl Session {
pub fn render(&self) -> Builder {
let mut builder = Response::builder();
builder.header(
CONTENT_SECURITY_POLICY,
format!(
concat!(
"default-src 'none'; ",
"script-src 'self' 'nonce-{nonce}' 'strict-dynamic'; ",
"style-src 'self' 'unsafe-inline'; ",
"connect-src 'self'; ",
"img-src * data:; ",
"object-src 'none'; ",
"base-uri 'none'; ",
"form-action 'self'; ",
"frame-ancestors 'none'",
),
nonce = self.nonce,
),
);
builder
}
}
use crate::models::session::Session;
use crate::templates::{self, RenderRucte};
use warp::http::Response;
use warp::{Rejection, Reply};
pub fn config() -> Result<impl Reply, Rejection> {
Response::builder().html(|o| templates::config(o))
pub fn config(session: Session) -> Result<impl Reply, Rejection> {
session.render().html(|o| templates::config(o, &session))
}
use crate::models::language::{Language, Selection};
use crate::models::paste::{ExternPaste, Paste};
use crate::models::session::Session;
use crate::schema::{languages, pastes};
use crate::templates::RenderRucte;
use crate::{templates, Connection};
use crate::templates::{self, RenderRucte};
use diesel::prelude::*;
use futures::future::*;
use futures03::TryFutureExt;
use tokio_executor::blocking;
use warp::http::Response;
use warp::{Rejection, Reply};
pub fn display_paste(
requested_identifier: String,
connection: Connection,
session: Session,
) -> impl Future<Item = impl Reply, Error = Rejection> {
blocking::run(move || {
Paste::delete_old(&connection)?;
let languages = Language::fetch(&connection)?;
let connection = &session.connection;
Paste::delete_old(connection)?;
let languages = Language::fetch(connection)?;
let paste: Paste = pastes::table
.inner_join(languages::table)
.select((
......@@ -26,14 +26,15 @@ pub fn display_paste(
languages::is_markdown,
))
.filter(pastes::identifier.eq(requested_identifier))
.get_result(&connection)
.get_result(connection)
.optional()
.map_err(warp::reject::custom)?
.ok_or_else(warp::reject::not_found)?;
let selected_language = Some(paste.language_id);
Response::builder().html(|o| {
session.render().html(|o| {
templates::display_paste(
o,
&session,
ExternPaste::from_paste(paste),
Selection {
languages,
......
use crate::models::language::{Language, Selection};
use crate::templates::RenderRucte;
use crate::{templates, Connection};
use crate::models::session::Session;
use crate::templates::{self, RenderRucte};
use futures::Future;
use futures03::TryFutureExt;
use tokio_executor::blocking;
use warp::http::Response;
use warp::{Rejection, Reply};
pub fn index(connection: Connection) -> impl Future<Item = impl Reply, Error = Rejection> {
pub fn index(session: Session) -> impl Future<Item = impl Reply, Error = Rejection> {
blocking::run(move || {
let languages = Language::fetch(&connection)?;
Response::builder().html(|o| {
let languages = Language::fetch(&session.connection)?;
session.render().html(|o| {
templates::index(
o,
&session,
Selection {
languages,
selected_language: None,
......
......@@ -7,6 +7,7 @@ mod insert_paste;
mod raw_paste;
mod run;
use crate::models::session::Session;
use crate::templates::{self, RenderRucte};
use crate::Connection;
use diesel::prelude::*;
......@@ -19,7 +20,7 @@ use warp::filters::BoxedFilter;
use warp::http::header::{
HeaderMap, HeaderValue, CONTENT_SECURITY_POLICY, REFERRER_POLICY, X_FRAME_OPTIONS,
};
use warp::http::{Response, StatusCode};
use warp::http::StatusCode;
use warp::{path, Filter, Rejection, Reply};
type PgPool = Pool<ConnectionManager<PgConnection>>;
......@@ -33,6 +34,18 @@ fn connection(pool: PgPool) -> BoxedFilter<(Connection,)> {
.boxed()
}
fn session(pool: PgPool) -> BoxedFilter<(Session,)> {
connection(pool)
.map(|connection| {
let bytes: [u8; 32] = rand::random();
Session {
nonce: base64::encode(&bytes),
connection,
}
})
.boxed()
}
fn index(pool: PgPool) -> BoxedFilter<(impl Reply,)> {
warp::path::end()
.and(
......@@ -41,7 +54,7 @@ fn index(pool: PgPool) -> BoxedFilter<(impl Reply,)> {
.and(warp::body::form())
.and(connection(pool.clone()))
.and_then(insert_paste::insert_paste)
.or(warp::get2().and(connection(pool)).and_then(index::index)),
.or(warp::get2().and(session(pool)).and_then(index::index)),
)
.boxed()
}
......@@ -50,15 +63,16 @@ fn display_paste(pool: PgPool) -> BoxedFilter<(impl Reply,)> {
warp::path::param()
.and(warp::path::end())
.and(warp::get2())
.and(connection(pool))
.and(session(pool))
.and_then(display_paste::display_paste)
.boxed()
}
fn options() -> BoxedFilter<(impl Reply,)> {
fn options(pool: PgPool) -> BoxedFilter<(impl Reply,)> {
warp::path("config")
.and(warp::path::end())
.and(warp::get2())
.and(session(pool))
.and_then(config::config)
.boxed()
}
......@@ -114,32 +128,22 @@ pub fn routes(
pool: Pool<ConnectionManager<PgConnection>>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> {
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_SECURITY_POLICY,
HeaderValue::from_static(concat!(
"default-src 'none'; ",
"script-src 'self'; ",
"style-src 'self' 'unsafe-inline'; ",
"connect-src 'self'; ",
"img-src * data:; ",
"object-src 'none'; ",
"base-uri 'none'; ",
"form-action 'self'; ",
"frame-ancestors 'none'",
)),
);
headers.insert(X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
headers.insert(REFERRER_POLICY, HeaderValue::from_static("no-referrer"));
index(pool.clone())
.or(favicon())
.or(options())
.or(options(pool.clone()))
.or(api_v0(pool.clone()))
.or(api_v1_languages(pool.clone()))
.or(raw_paste(pool.clone()))
.or(display_paste(pool.clone()))
.or(static_dir())
.recover(not_found)
.or(not_found(pool))
.with(warp::reply::with::headers(headers))
.with(warp::reply::with::default_header(
CONTENT_SECURITY_POLICY,
"default-src 'none'; frame-ancestors 'none'",
))
.with(warp::log("pastebinrun"))
}
......@@ -156,14 +160,15 @@ fn with_ext(ext: &'static str) -> impl Filter<Extract = (String,), Error = Rejec
})
}
fn not_found(rejection: Rejection) -> Result<impl Reply, Rejection> {
if rejection.is_not_found() {
Response::builder()
.status(StatusCode::NOT_FOUND)
.html(|o| templates::not_found(o))
} else {
Err(rejection)
}
fn not_found(pool: PgPool) -> BoxedFilter<(impl Reply,)> {
session(pool)
.and_then(|session: Session| {
session
.render()
.status(StatusCode::NOT_FOUND)
.html(|o| templates::not_found(o, &session))
})
.boxed()
}
#[cfg(test)]
......
@use crate::models::session::Session;
@use crate::templates::{footer, header};
@()
@:header()
@(session: &Session)
@:header(session)
<p id=options> This page requires a modern web browser with enabled JavaScript support.
@:footer()
@:footer(session)
@use crate::models::language::Selection;
@use crate::models::paste::ExternPaste;
@use crate::models::session::Session;
@use crate::templates::{buttons, header, footer, language_selection};
@(paste: ExternPaste, selection: Selection)
@(session: &Session, paste: ExternPaste, selection: Selection)
@:header()
@:header(session)
@Html(paste.markdown)
<form method="post" action="/" id="editor">
......@@ -21,4 +22,4 @@
<p><textarea id=code name=code>@('\n')@paste.paste</textarea></p>
@:buttons()
</form>
@:footer()
@:footer(session)
@()
@use crate::models::session::Session;
@(session: &Session)
</article>
<script src="/@env!("ENTRY_FILE_PATH")"></script>
<script src="/@env!("ENTRY_FILE_PATH")" nonce="@session.nonce"></script>
@()
@use crate::models::session::Session;
@(_session: &Session)
<!DOCTYPE html>
<meta charset=utf-8>
......
@use crate::models::language::Selection;
@use crate::models::session::Session;
@use crate::templates::{buttons, header, footer, language_selection};
@(selection: Selection)
@(session: &Session, selection: Selection)
@:header()
@:header(session)
<form method="post" action="/" id="editor">
<p>
@:language_selection(selection)
......@@ -14,4 +15,4 @@
<p><textarea id=code name=code></textarea></p>
@:buttons()
</form>
@:footer()
@:footer(session)
@use crate::models::session::Session;
@use crate::templates::{header, footer};
@()
@(session: &Session)
@:header()
@:header(session)
<h1>Paste not found</h1>
<p>It may have been automatically deleted after 24 hours.
@:footer()
@:footer(session)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment