{"record":{"id":"37a10dc80872a67c","repo":"jgraph/drawio-desktop","slug":"path-not-authorised","errorCode":null,"errorMessage":"path not authorised","messagePattern":"path not authorised","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/electron.js","lineNumber":3222,"sourceCode":"\t\t}\n\t}\n\n\treturn false;\n}\n\n// The renderer is semi-untrusted: it parses attacker-controlled diagram XML,\n// .vsdx, SVG, Mermaid, etc. validateSender is necessary but not sufficient,\n// because a renderer-side XSS attacker would also pass it. So write-side IPC\n// handlers must additionally confirm the requested path is one the user has\n// authorised through OS chrome (file picker, file association, argv) — see\n// blessPath. This function realpath-canonicalises the requested path\n// (defeating symlink traversal) and accepts only paths in blessedPaths or\n// their draft/backup siblings.\nasync function assertWritablePath(p)\n{\n\tif (typeof p !== 'string' || !p || p.includes('\\0'))\n\t{\n\t\tthrow new Error('path not authorised');\n\t}\n\n\tconst resolved = path.resolve(p);\n\tlet realpath;\n\n\ttry\n\t{\n\t\trealpath = await fsProm.realpath(resolved);\n\t}\n\tcatch (e)\n\t{\n\t\t// File doesn't exist yet (e.g. Save As to a new file). Canonicalise\n\t\t// the parent directory so symlinks in the directory chain are still\n\t\t// resolved.\n\t\ttry\n\t\t{\n\t\t\tconst parentReal = await fsProm.realpath(path.dirname(resolved));\n\t\t\trealpath = path.join(parentReal, path.basename(resolved));","sourceCodeStart":3204,"sourceCodeEnd":3240,"githubUrl":"https://github.com/jgraph/drawio-desktop/blob/403a2cb79f431db13a395f033b67eeebc1631754/src/main/electron.js#L3204-L3240","documentation":"First guard in assertWritablePath (src/main/electron.js:3220-3223): rejects any p that is not a non-empty string or that contains a NUL byte (defence against null-byte injection into Node fs APIs). This runs before realpath/blessedPaths checks, so no path ever reaches the filesystem with an embedded \\0.","triggerScenarios":"saveFile/saveDraft/writeFile/deleteFile invoked with a path that is undefined, null, a number, '', or a string containing '\\0' (e.g. from a malformed file association argv or a corrupted draft filename).","commonSituations":"A draft/backup filename was assembled from untrusted input that injected a NUL; an automated integration passed a non-string path; argv parsing yielded undefined for a CLI-opened file.","solutions":["Sanitize the path string before IPC: strip NUL bytes and reject non-strings up front.","Verify the field is set on the renderer side (assertWritablePath is the last line of defence; the IPC switch's reqStr usually catches missing strings first, but \\0 slips past reqStr).","If the path comes from argv, validate it is a real filesystem string before calling blessPath."],"exampleFix":"// before\nelectron.request({action: 'writeFile', path: tainted, data});\n\n// after\nconst safe = typeof tainted === 'string' && !tainted.includes('\\0') ? tainted : null;\nif (!safe) throw new TypeError('invalid path');\nelectron.request({action: 'writeFile', path: safe, data});","handlingStrategy":"validation","validationCode":"function isSafePath(p) {\n\treturn typeof p === 'string' && p.length > 0 && !p.includes('\\0');\n}\nif (!isSafePath(target)) throw new TypeError('unsafe path');","typeGuard":"const isSafePath = (p) => typeof p === 'string' && p.length > 0 && !p.includes('\\0');","tryCatchPattern":"try { await writeFile(target, data); }\ncatch (e) {\n\tif (e.message === 'path not authorised') { /* prompt user to pick a real path */ }\n\telse throw e;\n}","preventionTips":["Strip NUL bytes from any path derived from argv or untrusted input before IPC.","Validate path type at the preload boundary, not just in main.","Treat 'path not authorised' as a security event, log it for audit."],"tags":["security","path-traversal","validation","ipc"],"backgroundTag":null,"analyzedSha":"403a2cb79f431db13a395f033b67eeebc1631754","analyzedAt":"2026-08-13T00:15:02.468Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}