aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2021-07-03 11:50:29 +0200
committerMartin Fischer <martin@push-f.com>2021-07-03 20:23:22 +0200
commit306289dfd1b55b24eee3195ae56a6db7539b6740 (patch)
treea142988d2910ee5d096bcc6db12595c866f03905
parentf50225041545ecf71ead3e493203f16f4b5f24c0 (diff)
render .html files securely using IFrames
-rw-r--r--src/get_routes.rs37
-rw-r--r--src/main.rs51
-rw-r--r--src/post_routes.rs3
-rw-r--r--src/static/style.css2
-rw-r--r--src/static/style.css.sha2
5 files changed, 75 insertions, 20 deletions
diff --git a/src/get_routes.rs b/src/get_routes.rs
index e07050d..57d7e56 100644
--- a/src/get_routes.rs
+++ b/src/get_routes.rs
@@ -25,6 +25,7 @@ use crate::Context;
use crate::Error;
use crate::HyperResponse;
use crate::Page;
+use crate::RenderMode;
use crate::Response;
pub(crate) fn get_blob<C: Controller>(
@@ -40,7 +41,8 @@ pub(crate) fn get_blob<C: Controller>(
"upload" => Ok(forms::upload_form(true, controller, &ctx, parts).into()),
"log" => log_blob(entr, params, controller, ctx, parts),
"diff" => diff_blob(entr, params, controller, ctx, parts),
- "raw" => raw_blob(entr, params, controller, ctx, parts),
+ "raw" => raw_blob(entr, params, controller, ctx, parts).map(|r| r.into()),
+ "run" => run_blob(entr, params, controller, ctx, parts).map(|r| r.into()),
"move" => move_blob(entr, params, controller, ctx, parts),
"remove" => remove_blob(entr, params, controller, ctx, parts),
_ => Err(Error::BadRequest("unknown action".into())),
@@ -81,7 +83,7 @@ fn view_blob<C: Controller>(
match from_utf8(blob.content()) {
Ok(text) => {
if let Some(renderer) = get_renderer(&ctx.path) {
- renderer(text, &mut page);
+ renderer(text, &mut page, RenderMode::View);
} else {
page.body
.push_str(&format!("<pre>{}</pre>", html_escape(text)));
@@ -314,7 +316,7 @@ fn raw_blob<C: Controller>(
_controller: &C,
ctx: Context,
parts: &Parts,
-) -> Result<Response, Error> {
+) -> Result<HyperResponse, Error> {
if let Some(etag) = parts
.headers
.get(header::IF_NONE_MATCH)
@@ -348,7 +350,34 @@ fn raw_blob<C: Controller>(
.insert(header::CONTENT_TYPE, mime.to_string().parse().unwrap());
}
}
- Ok(resp.into())
+ Ok(resp)
+}
+
+fn run_blob<C: Controller>(
+ entr: TreeEntry,
+ params: ActionParam,
+ controller: &C,
+ ctx: Context,
+ parts: &Parts,
+) -> Result<HyperResponse, Error> {
+ if ctx.path.extension().unwrap().to_str() != Some("html") {
+ return Err(Error::BadRequest(
+ "run action only available for .html files".into(),
+ ));
+ }
+ raw_blob(entr, params, controller, ctx, parts).map(|mut r| {
+ r.headers_mut()
+ .insert(header::CONTENT_TYPE, "text/html".parse().unwrap());
+ // We want users to be able to view .html applications of other users
+ // without having to worry about the application accessing their private
+ // files. So we set the CSP: sandbox header which makes browsers treat
+ // the page as a unique origin as per the same-origin policy.
+ r.headers_mut().insert(
+ header::CONTENT_SECURITY_POLICY,
+ "sandbox allow-scripts;".parse().unwrap(),
+ );
+ r
+ })
}
fn move_blob<C: Controller>(
diff --git a/src/main.rs b/src/main.rs
index a5b7ce0..58431a2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -265,17 +265,18 @@ async fn service<C: Controller>(
});
// we rely on CSP to thwart XSS attacks, all modern browsers support it
- resp.headers_mut().insert(
- header::CONTENT_SECURITY_POLICY,
- format!(
- "default-src 'self'; frame-src {}; script-src {}; style-src {}",
- frame_csp,
- script_csp,
- include_str!("static/style.css.sha"),
- )
- .parse()
- .unwrap(),
- );
+ resp.headers_mut()
+ .entry(header::CONTENT_SECURITY_POLICY)
+ .or_insert_with(|| {
+ format!(
+ "default-src 'self'; frame-src {}; script-src {}; style-src {}",
+ frame_csp,
+ script_csp,
+ include_str!("static/style.css.sha"),
+ )
+ .parse()
+ .unwrap()
+ });
Ok(resp)
}
@@ -526,16 +527,40 @@ impl Context {
}
}
-fn render_markdown(input: &str, page: &mut Page) {
+#[derive(PartialEq)]
+enum RenderMode {
+ View,
+ Preview,
+}
+
+fn render_markdown(input: &str, page: &mut Page, _mode: RenderMode) {
let parser = Parser::new_ext(input, Options::all());
page.body.push_str("<div class=markdown-output>");
html::push_html(&mut page.body, parser);
page.body.push_str("</div>");
}
-fn get_renderer(path: &Path) -> Option<fn(&str, &mut Page)> {
+fn embed_html_as_iframe(input: &str, page: &mut Page, mode: RenderMode) {
+ if mode == RenderMode::View {
+ page.body.push_str("<iframe src='?action=run'></iframe>");
+ page.frame_src = Some("'self'");
+ } else {
+ page.body
+ .push_str("<div class=note>Note that JavaScript does not work in the preview.</div>");
+ // sandbox=allow-scripts wouldn't work because the strict parent page CSP still applies
+
+ // The sandbox attribute makes browsers treat the embedded page as a unique origin.
+ page.body.push_str(&format!(
+ "<iframe srcdoc='{}' sandbox></iframe>",
+ html_escape(input)
+ ));
+ }
+}
+
+fn get_renderer(path: &Path) -> Option<fn(&str, &mut Page, RenderMode)> {
match path.extension().map(|e| e.to_str().unwrap()) {
Some("md") => Some(render_markdown),
+ Some("html") => Some(embed_html_as_iframe),
_ => None,
}
}
diff --git a/src/post_routes.rs b/src/post_routes.rs
index 392a024..7588abc 100644
--- a/src/post_routes.rs
+++ b/src/post_routes.rs
@@ -20,6 +20,7 @@ use crate::get_renderer;
use crate::ActionParam;
use crate::Args;
use crate::Context;
+use crate::RenderMode;
use crate::Response;
use crate::{controller::Controller, Error};
@@ -363,6 +364,6 @@ async fn preview_edit<C: Controller>(
let form: EditForm = body.into_form().await?;
let new_text = form.text.replace("\r\n", "\n");
let mut page = edit_text_form(&form, None, controller, &ctx, parts);
- get_renderer(&ctx.path).unwrap()(&new_text, &mut page);
+ get_renderer(&ctx.path).unwrap()(&new_text, &mut page, RenderMode::Preview);
Ok(page.into())
}
diff --git a/src/static/style.css b/src/static/style.css
index 8ee788d..af60760 100644
--- a/src/static/style.css
+++ b/src/static/style.css
@@ -23,7 +23,7 @@ h1 {
flex-grow: 1;
}
-textarea {
+textarea, iframe {
flex-grow: 1;
}
diff --git a/src/static/style.css.sha b/src/static/style.css.sha
index 774a743..ed20f8e 100644
--- a/src/static/style.css.sha
+++ b/src/static/style.css.sha
@@ -1 +1 @@
-'sha256-9xIJ8fFnUyBYBKBAR6JWlmi39sUqyIqzUt1PnrkDE/c=' \ No newline at end of file
+'sha256-7wOMnVUwWmDEG4Yz3cqmo7y38q0qzzpSB9TiwkKMHmg=' \ No newline at end of file