Skip to content

fix(server): skip Host/CORS validation for wildcard bind addresses#486

Open
aung-i wants to merge 1 commit into
cline:mainfrom
aung-i:fix/host-allowed-0.0.0.0
Open

fix(server): skip Host/CORS validation for wildcard bind addresses#486
aung-i wants to merge 1 commit into
cline:mainfrom
aung-i:fix/host-allowed-0.0.0.0

Conversation

@aung-i

@aung-i aung-i commented May 13, 2026

Copy link
Copy Markdown

Summary

  • Skip Host header and CORS origin validation when the server binds to a wildcard address (0.0.0.0 or ::)
  • Add isKanbanWildcardHost() to detect wildcard bind addresses
  • evaluateHost() returns { kind: "allow" } early for wildcard binds, evaluateCors() skips origin comparison, getAllowedHostHeaders() returns an empty set
  • No security regression: passcode auth is still enforced because isKanbanRemoteHost() returns true for wildcard addresses

Problem

When running kanban --host 0.0.0.0, browsers send the external IP (e.g. 192.168.1.3:3484) in the Host header. Since 0.0.0.0 was in the allowlist but browsers never send Host: 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 check passes (lint + typecheck + 592 tests)
  • npm run build passes
  • New unit tests for isKanbanWildcardHost(), evaluateHost(), evaluateCors(), handleSocketUpgrade(), and getAllowedHostHeaders() with wildcard binding
  • Manually verified: kanban --host 0.0.0.0 accessible from external IP in port-forwarding setup

Fixes #426, fixes #482

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-apps

greptile-apps Bot commented May 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes wildcard-bind setups (--host 0.0.0.0 / --host ::) where browsers send the external IP in the Host header, causing every request to be rejected because the allowlist only contained the bind address itself.

  • Adds isKanbanWildcardHost() to detect 0.0.0.0/:: and short-circuits evaluateHost, evaluateCors, and getAllowedHostHeaders when it returns true, so any Host header and any Origin are accepted.
  • isKanbanRemoteHost() is deliberately unchanged: wildcard addresses are not in LOCALHOST_HOSTS, so passcode authentication is still required.
  • New unit tests cover all four functions under wildcard binding, and a top-level afterEach correctly restores the shared runtimeHost/runtimePort state between tests.

Confidence Score: 4/5

Safe 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.

Important Files Changed

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]
Loading
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

Comment thread src/server/middleware.ts
Comment on lines +32 to 43
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 };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

@fxerkan

fxerkan commented May 15, 2026

Copy link
Copy Markdown

Alternative Workaround Guide for Remote Access to Cline Kanban Board via Cloudflare

Audience: Developers who successfully installed the kanban CLI (Cline's task board) and want to
access it remotely through Cloudflare — but keep hitting a blank page, 403, or broken UI.


Why It Doesn't Work Out of the Box

kanban (the Cline Kanban board server) has two hardcoded security checks that reject any
request not coming from the expected local origin:

Check What Kanban expects What Cloudflare sends
Host header 127.0.0.1:3485 cline.yourdomain.com
Origin header http://127.0.0.1:3485 https://cline.yourdomain.com

When either check fails, Kanban silently rejects the request — you get a blank page or a CORS
error in the browser console. Simply pointing Cloudflare at port 3485 will not work.

The fix is a thin Node.js proxy that sits between Cloudflare and Kanban, rewriting those two
headers on every HTTP request and WebSocket upgrade before forwarding them upstream.


Architecture

Browser
  │  HTTPS
  ▼
Cloudflare  (cline.yourdomain.com)
  │  HTTP  →  127.0.0.1:3484
  ▼
kanban-proxy.js          ← rewrites Host + Origin headers
  │  HTTP  →  127.0.0.1:3485
  ▼
kanban server            ← accepts request (sees localhost origin)

Ports used:

  • 3485 — Cline Kanban's default port (do not expose this externally)
  • 3484 — the proxy's listening port (this is what Cloudflare points to)

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:LISTEN

Step 2 — Create the Proxy Script

Save this file as ~/.cline/kanban-proxy.js:

// 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 Proxy

node ~/.cline/kanban-proxy.js

You should see:

[kanban-proxy] 0.0.0.0:3484 → 127.0.0.1:3485
[kanban-proxy] Cloudflare: https://cline.yourdomain.com

Step 4 — Configure Cloudflare

In the Cloudflare Dashboard → Zero Trust → Tunnels (or Networks → Tunnels):

  1. Open your tunnel and go to Public Hostnames.
  2. Add a hostname entry:
    • Subdomain: cline
    • Domain: yourdomain.com
    • Service type: HTTP
    • URL: 127.0.0.1:3484
  3. Save. Cloudflare will automatically handle HTTPS termination.

If you are using a Cloudflare Application (Zero Trust Access) in front of this hostname,
make sure it is configured to allow your email/identity — otherwise you'll hit a login wall
before reaching the board.


Step 5 — Run Proxy as a macOS Launch Agent (so it starts on login)

Create ~/Library/LaunchAgents/com.cline.kanban-proxy.plist:

<?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.plist

Linux (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.target
systemctl --user enable --now kanban-proxy

Troubleshooting

Symptom Cause Fix
Blank page, no error Kanban rejected Host/Origin Confirm proxy is running on 3484 and kanban on 3485
CORS error in console Proxy not rewriting Access-Control-Allow-Origin Check you are running the proxy, not pointing CF directly at 3485
502 Bad Gateway Proxy up but kanban not running Start kanban serve first, then the proxy
WebSocket disconnect / board freezes WS upgrade not proxied Use the exact script above — the upgrade event handler is required
Cloudflare login wall CF Access policy not matching your identity Check Zero Trust → Access → your application policy
Works locally, fails remotely Port 3484 blocked by firewall CF Tunnel goes outbound — no inbound firewall rule needed

Key Insight

The proxy must be on the same machine as kanban serve.
Cloudflare Tunnel makes an outbound connection from your machine to Cloudflare's edge,
so no inbound firewall ports need to be opened — not even 3484.
The only requirement is that cloudflared is running and authenticated.


Verified on: macOS 15, Node.js 20+, Cline Kanban CLI, Cloudflare Zero Trust free plan.

@Francespo

Copy link
Copy Markdown

please guys fix this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Local Network Access issue can't expose over cloudflare tunnel with new origin configuration

3 participants