An upload asks the policy, like every other way in

/upload asked nothing. It took a file, capped it at 20 MB, and wrote
it to the bucket for whoever sent it - no session, no page, no policy.
It was the one server entry point that did not ask the question every
other one asks, and on a site whose forms are public that makes the
route public too: a free disk for anyone who finds the host.

Harmless while no instance had storage configured, which is why it sat
there. kasse now has a garage wired to klingenberg's portal, so it is
not harmless any more.

An upload now carries the question and the alternative the file is
being attached to. The page has to exist, the alternative has to be
open and to actually declare a `type: file` field, and the policy has
to allow Submit on that page for whoever is asking - checked after the
bytes are read and before any of them are written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-28 16:08:12 +02:00
co-authored by Claude Opus 5
parent 9891c462c6
commit 8d6f29011c
3 changed files with 187 additions and 44 deletions
+11 -2
View File
@@ -1042,7 +1042,7 @@ fn AlternativeCard(
};
let Some(files) = input.files() else { continue };
let Some(file) = files.get(0) else { continue };
match upload_file(&file).await {
match upload_file(&file, &question_id_for_submit, &alt_name_for_submit).await {
Ok(key) => {
map.insert(name.clone(), serde_json::Value::String(key));
}
@@ -2541,12 +2541,21 @@ pub async fn submit_answer(
/// server side is a raw Axum multipart handler (`src/upload.rs`) for
/// the same reason.
#[cfg(feature = "hydrate")]
async fn upload_file(file: &web_sys::File) -> Result<String, wasm_bindgen::JsValue> {
async fn upload_file(
file: &web_sys::File,
question_id: &str,
alternative: &str,
) -> Result<String, wasm_bindgen::JsValue> {
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{FormData, Request, RequestInit, Response};
let form = FormData::new()?;
// Which page and which choice this file is being attached to. The
// server refuses an upload that does not name a page it may
// submit, so these travel with every file.
form.append_with_str("question", question_id)?;
form.append_with_str("alternative", alternative)?;
form.append_with_blob("file", file)?;
let opts = RequestInit::new();