{"record":{"id":"c9d0a1ef7622a422","repo":"paperclipai/paperclip","slug":"devuiurl-must-target-localhost","errorCode":null,"errorMessage":"devUiUrl must target localhost","messagePattern":"devUiUrl must target localhost","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/src/routes/plugin-ui-static.ts","lineNumber":359,"sourceCode":"            res.status(400).json({ error: \"devUiUrl must use http or https protocol\" });\n            return;\n          }\n\n          // Dev proxy is restricted to loopback addresses only.\n          // Validate the *constructed* targetUrl hostname (not the base) to\n          // catch any path-based override that slipped past the checks above.\n          const devHost = targetUrl.hostname;\n          const isLoopback =\n            devHost === \"localhost\" ||\n            devHost === \"127.0.0.1\" ||\n            devHost === \"::1\" ||\n            devHost === \"[::1]\";\n          if (!isLoopback) {\n            log.warn(\n              { pluginId: plugin.id, devUiUrl, host: devHost },\n              \"plugin-ui-static: devUiUrl must target localhost, rejecting proxy\",\n            );\n            res.status(400).json({ error: \"devUiUrl must target localhost\" });\n            return;\n          }\n\n          log.debug(\n            { pluginId: plugin.id, devUiUrl, targetUrl: targetUrl.href },\n            \"plugin-ui-static: proxying to devUiUrl\",\n          );\n\n          try {\n            const controller = new AbortController();\n            const timeout = setTimeout(() => controller.abort(), 10_000);\n            try {\n              const upstream = await fetch(targetUrl.href, { signal: controller.signal });\n              if (!upstream.ok) {\n                res.status(upstream.status).json({\n                  error: `Dev server returned ${upstream.status}`,\n                });\n                return;","sourceCodeStart":341,"sourceCodeEnd":377,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/routes/plugin-ui-static.ts#L341-L377","documentation":"Returned as HTTP 400 by the plugin UI dev proxy (server/src/routes/plugin-ui-static.ts:359). Even with a valid http/https URL, the proxy only forwards to literal loopback hostnames: 'localhost', '127.0.0.1', '::1', or '[::1]'. The check runs on the *constructed* target URL hostname, so both a non-loopback devUiUrl and any path trick that changes the hostname are rejected.","triggerScenarios":"devUiUrl set to 'http://0.0.0.0:5173' (hostname '0.0.0.0' is not in the allowlist), 'http://192.168.1.20:5173', or an /etc/hosts alias like 'http://myplugin.localhost:5173' — none match the literal loopback strings, so any proxied asset request 400s. Also fires when the requested file path itself is crafted to change the hostname.","commonSituations":"Dev server bound to all interfaces and reached via the machine's LAN IP; using 0.0.0.0 as a 'same machine' shorthand; hostnames like *.localhost that browsers resolve but this literal comparison rejects; containerized setups where the plugin dev server is on another network namespace.","solutions":["Change devUiUrl to 'http://localhost:<port>/' or 'http://127.0.0.1:<port>/' — these are the only accepted hostnames","Make sure the dev server actually listens on loopback (vite dev --host localhost or default behavior)","If the dev server runs in a container or another host, port-forward it to loopback (e.g. kubectl port-forward, ssh -L) and point devUiUrl at the local forwarded port"],"exampleFix":"# before\n{ \"devUiUrl\": \"http://0.0.0.0:5173/\" }\n\n# after\n{ \"devUiUrl\": \"http://127.0.0.1:5173/\" }","handlingStrategy":"validation","validationCode":"const LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\"]);\nconst isLoopbackDevUiUrl = (u: string): boolean => {\n  try { return LOOPBACK_HOSTS.has(new URL(u).hostname); } catch { return false; }\n};","typeGuard":"const isLoopbackUrl = (u: string): boolean => {\n  try { return [\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\"].includes(new URL(u).hostname); } catch { return false; }\n};","tryCatchPattern":null,"preventionTips":["Use http://localhost:<port> or http://127.0.0.1:<port> — '0.0.0.0' and LAN IPs are rejected","Port-forward remote/containerized dev servers to loopback instead of pointing devUiUrl at them directly"],"tags":["plugins","dev-proxy","ssrf","loopback","http-400","configuration"],"backgroundTag":"ssrf-loopback-guard","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}