Plugins: refuse file and network access through path params and collab (#723)

Plugins can no longer reach the disk or network through path parameters (variable.export/codeConnect.export/component.playground writes, codeConnect.import/variable.import reads) or through collab.* and window.*; found and fixed by @lucasmartins-ai, split out of #696.
This commit is contained in:
BFlat
2026-10-17 13:35:00 +07:00
committed by GitHub
parent 40c744da10
commit 69806c9edd
3 changed files with 27 additions and 1 deletions
+15
View File
@@ -111,6 +111,10 @@ const DENIED: &[&str] = &[
"edit.redo",
"edit.repeat",
"command.batch",
// Network sessions, online files and their recovery copies on disk.
"collab.",
// Opens files in new windows.
"window.",
"view.theme",
];
/// Read-only exceptions to [`DENIED`].
@@ -128,6 +132,17 @@ fn refusal(m: &Manifest, command: &str, params: &Value) -> Option<String> {
if command == "image.place" && get(params, "path").is_some() {
return Some("plugins can't read files: pass the image as `data`".into());
}
// `path` here is a file on disk (elsewhere it is a vector path or an action's place).
if matches!(command, "variable.export" | "codeConnect.export" | "component.playground") && get(params, "path").is_some() {
return Some("plugins can't write files: leave out `path` to get the result back".into());
}
if command == "codeConnect.import" && get(params, "path").is_some() {
return Some("plugins can't read files: pass the connections as `json`".into());
}
// Variables JSON may be given as a path: only objects and JSON text for plugins.
if command == "variable.import" && get(params, "variables").and_then(Value::as_str).is_some_and(|t| !t.trim_start().starts_with('{')) {
return Some("plugins can't read files: pass the variables JSON itself".into());
}
if !m.permissions.allows(command) {
return Some(format!("`{command}` isn't in the commands {} asked permission for", m.name));
}
+7
View File
@@ -169,6 +169,13 @@ fn files_dialogs_history_and_plugins_are_off_limits() {
("plugin.remove", json!({"id": "org.test.deny"})),
("edit.undo", json!({})),
("command.batch", json!({"commands": [{"command": "file.saveAs", "params": {"path": target}}]})),
("variable.export", json!({"format": "css", "path": target})),
("codeConnect.export", json!({"path": target})),
("codeConnect.import", json!({"path": "/etc/passwd"})),
("component.playground", json!({"id": a, "path": target})),
("variable.import", json!({"variables": "/etc/passwd"})),
("collab.recoverFile", json!({"path": "copy.figcraft", "file": "x", "session": 1, "client": 1, "seq": 0})),
("window.new", json!({"path": "C:/Windows/win.ini"})),
("doc.switch", json!({"index": 0})),
("pref.keepTool", json!({})),
// Without a name it would open inline renaming in the layers panel: a UI request.
+5 -1
View File
@@ -237,7 +237,11 @@ Whatever its manifest says, FigCraft refuses these calls (the plugin gets the re
it can handle):
- files and the app: `file.*`, `app.*`, `pref.*`, `help.*`, `library.add`, `libraries.*`,
`image.place` with a `path` (pass the image as base64 `data` instead), `view.theme*`;
`image.place` with a `path` (pass the image as base64 `data` instead), `variable.export`,
`codeConnect.export` and `component.playground` with a `path` (leave it out to get the result
back), `codeConnect.import` with a `path` and `variable.import` with a file path instead of JSON,
`window.*` (new windows open files), `view.theme*`;
- live sessions and online files: `collab.*`;
- other documents and the version history: `doc.*` and `version.*` (except `doc.list` and
`version.list`);
- the undo history and nesting: `edit.undo`, `edit.redo`, `edit.repeat`, `command.batch` (a run is