mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-24 21:40:48 +02:00
fix(fs): reject malformed write_file options instead of ignoring them (#3622)
In plugins/fs/src/commands.rs write_file_inner, a malformed or non-ASCII `options` header was dropped with .ok(), so baseDir/append/createNew were silently ignored and the call became a truncating write to the raw path; the JSON array body fallback truncated values above 255 and dropped non-numbers. The header is parsed with parse_write_file_options: empty, `undefined` (what fetch sends for JSON.stringify(undefined)) and `null` mean no options, anything else must parse. Array bodies must only contain bytes. Unit test for the header parser; e2e spec for writes without options, which is the case the `undefined` handling keeps working.
This commit is contained in:
@@ -1093,11 +1093,12 @@ async fn write_file_inner<R: Runtime>(
|
||||
})
|
||||
.and_then(|p| SafeFilePath::from_str(&p).map_err(CommandError::from))?;
|
||||
|
||||
let options: Option<WriteFileOptions> = request
|
||||
let options = request
|
||||
.headers()
|
||||
.get("options")
|
||||
.and_then(|p| p.to_str().ok())
|
||||
.and_then(|opts| serde_json::from_str(opts).ok());
|
||||
.map(|options| parse_write_file_options(options.as_bytes()))
|
||||
.transpose()?
|
||||
.flatten();
|
||||
|
||||
let mut file_handle = resolve_file(
|
||||
permission,
|
||||
@@ -1140,8 +1141,12 @@ async fn write_file_inner<R: Runtime>(
|
||||
tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data),
|
||||
tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned(
|
||||
data.iter()
|
||||
.flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8)))
|
||||
.collect(),
|
||||
.map(|v| {
|
||||
v.as_u64()
|
||||
.and_then(|v| u8::try_from(v).ok())
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid byte in the data to write: {v}"))
|
||||
})
|
||||
.collect::<Result<Vec<u8>, _>>()?,
|
||||
),
|
||||
_ => return Err(anyhow::anyhow!("unexpected invoke body").into()),
|
||||
};
|
||||
@@ -1157,6 +1162,20 @@ async fn write_file_inner<R: Runtime>(
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Parses the `options` header of the `write_file` command.
|
||||
///
|
||||
/// Fails instead of silently falling back to the defaults, which would e.g. ignore `baseDir`.
|
||||
fn parse_write_file_options(header: &[u8]) -> CommandResult<Option<WriteFileOptions>> {
|
||||
let header = String::from_utf8_lossy(header);
|
||||
match header.trim() {
|
||||
// `JSON.stringify(undefined)` is sent as `undefined` by `fetch`
|
||||
"" | "undefined" | "null" => Ok(None),
|
||||
options => serde_json::from_str(options)
|
||||
.map(Some)
|
||||
.map_err(|e| anyhow::anyhow!("invalid write file options {options}: {e}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_file<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
@@ -1810,6 +1829,25 @@ mod test {
|
||||
|
||||
use super::LinesBytes;
|
||||
|
||||
#[test]
|
||||
fn write_file_options_header() {
|
||||
use super::parse_write_file_options;
|
||||
|
||||
assert!(parse_write_file_options(b"undefined").unwrap().is_none());
|
||||
assert!(parse_write_file_options(b"null").unwrap().is_none());
|
||||
assert!(parse_write_file_options(b"").unwrap().is_none());
|
||||
|
||||
let options = parse_write_file_options(br#"{"baseDir":14,"append":true}"#)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(options.append);
|
||||
assert!(options.create);
|
||||
assert!(options.base.base_dir.is_some());
|
||||
|
||||
assert!(parse_write_file_options(b"{not json").is_err());
|
||||
assert!(parse_write_file_options(br#"{"append":"yes"}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_file_path_parse() {
|
||||
use super::SafeFilePath;
|
||||
|
||||
Reference in New Issue
Block a user