fix(server): skip Host/CORS validation for wildcard bind addresses#486
fix(server): skip Host/CORS validation for wildcard bind addresses#486aung-i wants to merge 1 commit into
Conversation
When Kanban binds to 0.0.0.0 or ::, browsers send the external IP (e.g. 192.168.1.3:3484) in the Host header, which is never in the allowlist. This causes all requests to be rejected with "Host not allowed." even though the user explicitly chose to expose the server. Skip Host header and CORS origin validation when bound to a wildcard address. Security is maintained by passcode auth, which is still required for remote access (isKanbanRemoteHost returns true for wildcard addresses). Fixes cline#426, fixes cline#482
Greptile SummaryThis PR fixes wildcard-bind setups (
Confidence Score: 4/5Safe to merge for the documented use case; the only trade-off is the intentional removal of Host and CORS defenses for wildcard binds, leaving passcode auth as the sole protection layer. The logic is correct and the fix is well-scoped. The CORS path now reflects any request origin back with Access-Control-Allow-Credentials: true for wildcard binds, fully removing DNS-rebinding protection. This matches Vite/Webpack behavior and is explicitly documented, but depends on passcode auth being active — users without a passcode on a shared network are exposed. src/server/middleware.ts — specifically the CORS credential-reflection behavior when wildcard is active.
|
| Filename | Overview |
|---|---|
| src/server/middleware.ts | evaluateHost, evaluateCors, and getAllowedHostHeaders all short-circuit for wildcard binds; logic is correct but CORS now echoes any request origin with Access-Control-Allow-Credentials: true, fully removing the DNS-rebinding defense layer for wildcard setups. |
| src/core/runtime-endpoint.ts | Adds WILDCARD_HOSTS set and isKanbanWildcardHost() to detect 0.0.0.0/:: bind addresses; isKanbanRemoteHost() is unchanged, correctly returning true for wildcards so passcode auth remains enforced. |
| test/runtime/server/middleware.test.ts | Adds afterEach teardown for runtime host/port state, plus thorough wildcard tests across evaluateHost, handleSocketUpgrade, getAllowedHostHeaders, and evaluateCors; covers both 0.0.0.0 and :: variants. |
| test/runtime/runtime-endpoint.test.ts | New describe block covers all four isKanbanWildcardHost cases (0.0.0.0, ::, 127.0.0.1, external IP); test isolation via setKanbanRuntimeHost is consistent with the rest of the file. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming Request] --> B{isKanbanWildcardHost?}
B -- Yes --> C[evaluateHost: allow immediately]
B -- No --> D{hostHeader present and in allowedHosts?}
D -- No --> E[Reject 403: Host not allowed]
D -- Yes --> F[evaluateCors check]
C --> G{origin header present?}
G -- No --> H[allow, origin=null]
G -- Yes --> I[skip origin comparison, pass through to preflight check]
F --> J{origin matches allowedOrigin or devServer?}
J -- No --> K[Reject 403: Origin not allowed]
J -- Yes --> L{OPTIONS preflight?}
I --> L
L -- Yes --> M[preflight: echo origin + Access-Control-Allow-Credentials: true]
L -- No --> N[allow: echo origin + Access-Control-Allow-Credentials: true]
H --> O[Request proceeds]
N --> O
M --> P[204 No Content]
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
src/server/middleware.ts:32-43
**CORS credential reflection allows any cross-origin credentialed request for wildcard binds**
When `isKanbanWildcardHost()` is true and the request has an `Origin` header, the code falls through to `applyAllowedOriginHeaders`, which sets both `Access-Control-Allow-Origin: <echoed-origin>` and `Access-Control-Allow-Credentials: true` for every origin. This completely removes the DNS-rebinding protection layer: a malicious page can rebind its domain to the server's IP, then make credentialed cross-origin requests to the kanban API. Passcode auth is the sole remaining defense.
This is intentional and documented, and matches how Vite/Webpack dev servers behave — but it is worth confirming that the intended threat model accepts this trade-off for users running with `--host 0.0.0.0` (e.g., a shared office network vs. a controlled home LAN).
Reviews (1): Last reviewed commit: "fix(server): skip Host/CORS validation f..." | Re-trigger Greptile
| if (!isKanbanWildcardHost()) { | ||
| const isDevServer = isDev && (origin === "http://localhost:4173" || origin === "http://127.0.0.1:4173"); | ||
| if (origin !== input.allowedOrigin && !isDevServer) { | ||
| return { kind: "reject", origin }; | ||
| } | ||
| } | ||
|
|
||
| if (isPreflight) { | ||
| return { kind: "preflight", origin }; | ||
| } | ||
|
|
||
| return { kind: "allow", origin }; | ||
| } |
There was a problem hiding this comment.
CORS credential reflection allows any cross-origin credentialed request for wildcard binds
When isKanbanWildcardHost() is true and the request has an Origin header, the code falls through to applyAllowedOriginHeaders, which sets both Access-Control-Allow-Origin: <echoed-origin> and Access-Control-Allow-Credentials: true for every origin. This completely removes the DNS-rebinding protection layer: a malicious page can rebind its domain to the server's IP, then make credentialed cross-origin requests to the kanban API. Passcode auth is the sole remaining defense.
This is intentional and documented, and matches how Vite/Webpack dev servers behave — but it is worth confirming that the intended threat model accepts this trade-off for users running with --host 0.0.0.0 (e.g., a shared office network vs. a controlled home LAN).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/middleware.ts
Line: 32-43
Comment:
**CORS credential reflection allows any cross-origin credentialed request for wildcard binds**
When `isKanbanWildcardHost()` is true and the request has an `Origin` header, the code falls through to `applyAllowedOriginHeaders`, which sets both `Access-Control-Allow-Origin: <echoed-origin>` and `Access-Control-Allow-Credentials: true` for every origin. This completely removes the DNS-rebinding protection layer: a malicious page can rebind its domain to the server's IP, then make credentialed cross-origin requests to the kanban API. Passcode auth is the sole remaining defense.
This is intentional and documented, and matches how Vite/Webpack dev servers behave — but it is worth confirming that the intended threat model accepts this trade-off for users running with `--host 0.0.0.0` (e.g., a shared office network vs. a controlled home LAN).
How can I resolve this? If you propose a fix, please make it concise.
Alternative Workaround Guide for Remote Access to Cline Kanban Board via Cloudflare
Why It Doesn't Work Out of the Box
When either check fails, Kanban silently rejects the request — you get a blank page or a CORS The fix is a thin Node.js proxy that sits between Cloudflare and Kanban, rewriting those two ArchitecturePorts used:
Step 1 — Verify Kanban is Running# Start Kanban (if not already running as a daemon)
kanban serve
# Confirm it's on port 3485
lsof -iTCP:3485 -sTCP:LISTENStep 2 — Create the Proxy ScriptSave this file as // Kanban Host + Origin Rewriting HTTP Proxy
//
// Cline Kanban has two security checks:
// 1. Host header must match the bound host (127.0.0.1:3485)
// 2. Origin header must match getKanbanRuntimeOrigin() = http://127.0.0.1:3485
//
// This proxy rewrites both on every request so Kanban always accepts them,
// regardless of what external hostname the client used (LAN IP, Tailscale, Cloudflare).
//
// Listens on: 0.0.0.0:3484
// Forwards to: 127.0.0.1:3485 (kanban)
const http = require('http');
const net = require('net');
const PROXY_PORT = 3484;
const TARGET_HOST = '127.0.0.1';
const TARGET_PORT = 3485;
const KANBAN_HOST = `${TARGET_HOST}:${TARGET_PORT}`; // 127.0.0.1:3485
const KANBAN_ORIGIN = `http://${TARGET_HOST}:${TARGET_PORT}`; // http://127.0.0.1:3485
// ── HTTP requests ────────────────────────────────────────────────────────────
const server = http.createServer((req, res) => {
const clientOrigin = req.headers['origin'] || null;
// Rewrite so Kanban's security checks pass
req.headers['host'] = KANBAN_HOST;
if (clientOrigin) req.headers['origin'] = KANBAN_ORIGIN;
// Strip Referer — it can expose the external hostname to Kanban's checks
delete req.headers['referer'];
const proxyReq = http.request(
{ hostname: TARGET_HOST, port: TARGET_PORT,
path: req.url, method: req.method, headers: req.headers },
(proxyRes) => {
const resHeaders = { ...proxyRes.headers };
// Rewrite CORS response header back to the real client origin
if (clientOrigin && resHeaders['access-control-allow-origin']) {
resHeaders['access-control-allow-origin'] = clientOrigin;
}
if (clientOrigin) {
resHeaders['access-control-allow-credentials'] = 'true';
}
res.writeHead(proxyRes.statusCode, resHeaders);
proxyRes.pipe(res, { end: true });
}
);
proxyReq.on('error', (err) => {
console.error('[kanban-proxy] upstream error:', err.message);
if (!res.headersSent) { res.writeHead(502); res.end('Bad Gateway'); }
});
req.pipe(proxyReq, { end: true });
});
// ── WebSocket upgrades ───────────────────────────────────────────────────────
server.on('upgrade', (req, socket, head) => {
req.headers['host'] = KANBAN_HOST;
if (req.headers['origin']) req.headers['origin'] = KANBAN_ORIGIN;
delete req.headers['referer'];
const targetSocket = net.connect(TARGET_PORT, TARGET_HOST, () => {
let hdrs = `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`;
for (const [k, v] of Object.entries(req.headers)) hdrs += `${k}: ${v}\r\n`;
hdrs += '\r\n';
targetSocket.write(hdrs);
if (head && head.length > 0) targetSocket.write(head);
socket.pipe(targetSocket);
targetSocket.pipe(socket);
});
const cleanup = () => { targetSocket.destroy(); socket.destroy(); };
targetSocket.on('error', cleanup);
socket.on('error', cleanup);
});
// ── Start ────────────────────────────────────────────────────────────────────
server.listen(PROXY_PORT, '0.0.0.0', () => {
console.log(`[kanban-proxy] 0.0.0.0:${PROXY_PORT} → ${TARGET_HOST}:${TARGET_PORT}`);
console.log(`[kanban-proxy] Cloudflare: https://cline.yourdomain.com`);
});
server.on('error', (err) => { console.error('[kanban-proxy] Error:', err.message); process.exit(1); });Step 3 — Start the Proxynode ~/.cline/kanban-proxy.jsYou should see: Step 4 — Configure CloudflareIn the Cloudflare Dashboard → Zero Trust → Tunnels (or Networks → Tunnels):
Step 5 — Run Proxy as a macOS Launch Agent (so it starts on login)Create <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.cline.kanban-proxy</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/env</string>
<string>node</string>
<string>/Users/YOUR_USERNAME/.cline/kanban-proxy.js</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/kanban-proxy.log</string>
<key>StandardErrorPath</key>
<string>/tmp/kanban-proxy.err</string>
</dict>
</plist>launchctl load ~/Library/LaunchAgents/com.cline.kanban-proxy.plistLinux (systemd): # ~/.config/systemd/user/kanban-proxy.service
[Unit]
Description=Cline Kanban Proxy
After=network.target
[Service]
ExecStart=/usr/bin/node /home/YOUR_USERNAME/.cline/kanban-proxy.js
Restart=on-failure
[Install]
WantedBy=default.targetsystemctl --user enable --now kanban-proxyTroubleshooting
Key Insight
Verified on: macOS 15, Node.js 20+, Cline Kanban CLI, Cloudflare Zero Trust free plan. |
|
please guys fix this |
Summary
0.0.0.0or::)isKanbanWildcardHost()to detect wildcard bind addressesevaluateHost()returns{ kind: "allow" }early for wildcard binds,evaluateCors()skips origin comparison,getAllowedHostHeaders()returns an empty setisKanbanRemoteHost()returnstruefor wildcard addressesProblem
When running
kanban --host 0.0.0.0, browsers send the external IP (e.g.192.168.1.3:3484) in the Host header. Since0.0.0.0was in the allowlist but browsers never sendHost: 0.0.0.0, all requests were rejected with{"error":"Host not allowed."}. The same issue affects Cloudflare tunnels and port-forwarding scenarios where the external IP is not visible inside the container.This matches the behavior of Vite (
allowedHosts: true+cors: true) and Webpack dev server (allowedHosts: 'all'), which skip Host validation when explicitly bound to all interfaces.Test plan
npm run checkpasses (lint + typecheck + 592 tests)npm run buildpassesisKanbanWildcardHost(),evaluateHost(),evaluateCors(),handleSocketUpgrade(), andgetAllowedHostHeaders()with wildcard bindingkanban --host 0.0.0.0accessible from external IP in port-forwarding setupFixes #426, fixes #482