{"record":{"id":"08a41459dba32e91","repo":"paperclipai/paperclip","slug":"invalid-file-path","errorCode":null,"errorMessage":"Invalid file path","messagePattern":"Invalid file path","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"server/src/routes/plugin-ui-static.ts","lineNumber":324,"sourceCode":"        // Dev proxy is only available in development mode\n        if (process.env.NODE_ENV === \"production\") {\n          log.warn(\n            { pluginId: plugin.id },\n            \"plugin-ui-static: devUiUrl ignored in production\",\n          );\n          // Fall through to static file serving below\n        } else {\n          // Guard against rawFilePath overriding the base URL via protocol\n          // scheme (e.g. \"https://evil.com/x\") or protocol-relative paths\n          // (e.g. \"//evil.com/x\") which cause `new URL(path, base)` to\n          // ignore the base entirely.\n          // Normalize percent-encoding so encoded slashes (%2F) can't bypass\n          // the protocol/path checks below.\n          let decodedPath: string;\n          try {\n            decodedPath = decodeURIComponent(rawFilePath);\n          } catch {\n            res.status(400).json({ error: \"Invalid file path\" });\n            return;\n          }\n          if (\n            decodedPath.includes(\"://\") ||\n            decodedPath.startsWith(\"//\") ||\n            decodedPath.startsWith(\"\\\\\\\\\")\n          ) {\n            res.status(400).json({ error: \"Invalid file path\" });\n            return;\n          }\n\n          // Proxy the request to the dev server\n          const targetUrl = new URL(rawFilePath, devUiUrl.endsWith(\"/\") ? devUiUrl : devUiUrl + \"/\");\n\n          // SSRF protection: only allow http/https and localhost targets for dev proxy\n          if (targetUrl.protocol !== \"http:\" && targetUrl.protocol !== \"https:\") {\n            res.status(400).json({ error: \"devUiUrl must use http or https protocol\" });\n            return;","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/routes/plugin-ui-static.ts#L306-L342","documentation":"Returned as HTTP 400 by GET /_plugins/:pluginId/ui/* (server/src/routes/plugin-ui-static.ts:324) when the dev-proxy branch is active (a devUiUrl is configured in company plugin config and NODE_ENV is not production) and the wildcard file path cannot be percent-decoded. decodeURIComponent throws on malformed escape sequences such as a lone '%' or '%zz', and the route converts that into a 400.","triggerScenarios":"GET /_plugins/<id>/ui/badge%.png, /_plugins/<id>/ui/file%E0%A4%x.js, or any path containing an invalid percent-encoding, while the plugin has devUiUrl set for hot-reload development. Only reachable in the dev-proxy code path; in production or without devUiUrl the malformed path is handled by the static-file branch instead.","commonSituations":"Hand-typed or truncated asset URLs; filenames containing literal '%' that were not encodeURI'd when constructing the request; double-encoding bugs where a client encodes an already-encoded path (e.g. '%25' then mangled again); security probes with garbage encodings.","solutions":["Fix the requesting URL so it is a valid percent-encoded path (a literal '%' must be sent as '%25')","When building asset URLs in code, always run the path through encodeURI()/encodeURIComponent instead of string concatenation","If a source file genuinely contains '%' in its name, rename it during the plugin build so emitted asset URLs stay clean","Confirm you actually want the dev proxy — without a devUiUrl in plugin config this branch never runs"],"exampleFix":"// before\nconst url = `/_plugins/${pluginId}/ui/${rawFilePath}`; // rawFilePath may contain '%'\n\n// after\nconst url = `/_plugins/${pluginId}/ui/${encodeURI(rawFilePath)}`;","handlingStrategy":"validation","validationCode":"const isDecodablePath = (p: string): boolean => {\n  try {\n    decodeURIComponent(p);\n    return true;\n  } catch {\n    return false;\n  }\n};\nconst safePath = isDecodablePath(filePath) ? encodeURI(filePath) : null;","typeGuard":"const isSafeAssetPath = (p: string): boolean =>\n  isDecodablePath(p) && !p.includes(\"://\") && !p.startsWith(\"//\") && !p.startsWith(\"\\\\\\\\\");","tryCatchPattern":null,"preventionTips":["Always construct asset URLs with encodeURI/encodeURIComponent, never raw concatenation of filenames that may contain '%'","Keep '%' characters out of built asset filenames (rename at build time)"],"tags":["plugins","dev-proxy","percent-encoding","http-400","url-encoding"],"backgroundTag":"malformed-percent-encoding","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}