diff --git a/examples/official-site/sqlpage/migrations/39_persist_uploaded_file.sql b/examples/official-site/sqlpage/migrations/39_persist_uploaded_file.sql index b45943aa..76619a2d 100644 --- a/examples/official-site/sqlpage/migrations/39_persist_uploaded_file.sql +++ b/examples/official-site/sqlpage/migrations/39_persist_uploaded_file.sql @@ -52,7 +52,9 @@ VALUES ( 'persist_uploaded_file', 2, 'destination_folder', - 'Optional. Path to the folder where the file will be saved, relative to the web root (the root folder of your website files). By default, the file will be saved in the `uploads` folder.', + 'Optional. Path to the folder where the file will be saved, relative to the web root (the root folder of your website files). By default, the file will be saved in the `uploads` folder. + +**Security note**: this value must be a folder name you choose yourself in your SQL code (a trusted constant). Never build it from untrusted input such as a form field, a query parameter, a request header, or anything else the visitor controls. The folder is joined directly to the web root, so a value containing `..` or an absolute path would cause the uploaded file to be written *outside* the web root, anywhere the SQLPage process can write. Keeping `destination_folder` a fixed value chosen by the application author avoids this.', 'TEXT' ), ( diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index bf669470..7710ef27 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -512,7 +512,12 @@ async fn persist_uploaded_file<'a>( let exts = allowed_extensions.collect::>().join(", "); anyhow::bail!("file extension {extension} is not allowed. Allowed extensions: {exts}"); } - // resolve the folder path relative to the web root + // Resolve the folder path relative to the web root. + // `folder` is trusted application input: it is expected to be a constant chosen by the + // app author in their SQL code, never attacker-controlled request data. It is joined + // directly to the web root, so a `folder` containing `..` or an absolute path would let + // the caller write the uploaded file outside the web root. Callers must not pass + // untrusted input (form fields, query parameters, headers, ...) as the folder. let web_root = &request.app_state.config.web_root; let target_folder = web_root.join(&*folder); // create the folder if it doesn't exist