{"record":{"id":"0eb82627bd216ef3","repo":"windmill-labs/windmill","slug":"raw-app-path-json-stringify-relpath-escapes-th","errorCode":null,"errorMessage":"raw app path ${JSON.stringify(relPath)} escapes the app folder ${baseFolder}","messagePattern":"raw app path (.+?) escapes the app folder (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/commands/sync/sync.ts","lineNumber":1134,"sourceCode":"/**\n * Join a raw app's author-controlled key (`value.files` path, `value.runnables`\n * id) under `baseFolder` and refuse anything that resolves outside it. Keys are\n * remote data written to disk on pull, so a `..` segment must not walk a written\n * file out of the app's own folder.\n */\nexport function rawAppPathWithinFolder(\n  baseFolder: string,\n  relPath: string,\n): string {\n  const resolved = path.join(baseFolder, relPath);\n  const rel = path.relative(baseFolder, resolved);\n  if (\n    rel === \"\" ||\n    rel === \"..\" ||\n    rel.startsWith(\"..\" + path.sep) ||\n    path.isAbsolute(rel)\n  ) {\n    throw new Error(\n      `raw app path ${JSON.stringify(relPath)} escapes the app folder ${baseFolder}`,\n    );\n  }\n  return resolved;\n}\n\nexport function ZipFSElement(\n  zip: JSZip,\n  useYaml: boolean,\n  defaultTs: \"bun\" | \"deno\",\n  resourceTypeToFormatExtension: Record<string, string>,\n  resourceTypeToIsFileset: Record<string, boolean>,\n  ignoreCodebaseChanges: boolean,\n  stripOnBehalfOf: boolean,\n  // Names a flow's rendered inline-script files after the checkout's own\n  // `!inline` references (module id -> file). The export carries script\n  // source, never a reference, so without a checkout to defer to every file\n  // is named from the step summary, and a file the checkout names otherwise","sourceCodeStart":1116,"sourceCodeEnd":1152,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/cli/src/commands/sync/sync.ts#L1116-L1152","documentation":"rawAppPathWithinFolder() joins an author-controlled key from a raw app definition (e.g. a `value.files` path or a runnable id) under the app's local folder and verifies the result stays inside it. It throws when the key contains `..` segments or is absolute, so the resolved path would escape the app folder. This is a security guard: raw app content is remote data, and without the check a malicious app could cause files to be written outside its own directory on pull.","triggerScenarios":"Pulling/syncing a raw app whose `value.files` entries or runnable ids contain path traversal like `../../etc/x`, an absolute path (`/abs/...`), or an empty/'..' key — whether authored accidentally or maliciously in a shared app.","commonSituations":"Importing a raw app exported from another instance where files were stored with absolute paths; hand-editing an app's JSON and introducing `..` in a file key; receiving a shared community app with hostile keys.","solutions":["Inspect the raw app definition's `value.files` and runnable keys; remove `..` segments and absolute paths so keys are relative names inside the app folder","Re-export the app with relative file keys and re-sync","If you trust the source but it legitimately needs nested paths, keep them within the folder (`sub/dir/file.ext` is fine; `../outside` is not)","Never bypass this check for untrusted apps — it prevents arbitrary file writes on your machine"],"exampleFix":"// before (in app JSON)\n\"files\": { \"../../secrets.txt\": \"...\" }\n// after\n\"files\": { \"assets/secrets.txt\": \"...\" }","handlingStrategy":"validation","validationCode":"function isSafeRelPath(p: string): boolean {\n  return !!p && !path.isAbsolute(p) && !p.split(/[\\\\/]/).includes('..');\n}\n// validate every value.files key / runnable id before syncing","typeGuard":"function staysInFolder(baseFolder: string, rel: string): boolean {\n  const r = path.relative(baseFolder, path.resolve(baseFolder, rel));\n  return r !== '' && r !== '..' && !r.startsWith('..' + path.sep) && !path.isAbsolute(r);\n}","tryCatchPattern":"try {\n  const p = rawAppPathWithinFolder(baseFolder, key);\n} catch (e) {\n  // reject/skip the hostile key; do NOT widen the folder to silence it\n  reportSkippedFile(key);\n}","preventionTips":["Treat raw app file keys as untrusted remote data — always relative, no '..',' no absolute paths","Validate keys when authoring or importing raw apps from external sources","Never bypass rawAppPathWithinFolder for shared/community apps"],"tags":["security","path-traversal","raw-app","sync"],"backgroundTag":"path-traversal-blocked","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}