ruflo 3.56.10 → 3.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruflo",
3
- "version": "3.56.10",
3
+ "version": "3.57.0",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration platform. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "bin/ruflo.js",
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "package:rvf": "bash src/scripts/package-rvf.sh"
41
41
  },
42
42
  "dependencies": {
43
- "@claude-flow/cli": "3.56.10"
43
+ "@claude-flow/cli": "3.57.0"
44
44
  },
45
45
  "optionalDependencies": {},
46
46
  "overrides": {
@@ -890,6 +890,7 @@ app.use((req, res, next) => {
890
890
  const CORS_ALLOWLIST = (process.env.MCP_CORS_ORIGIN || "*")
891
891
  .split(",").map(s => s.trim()).filter(Boolean);
892
892
  const CORS_WILDCARD = CORS_ALLOWLIST.length === 1 && CORS_ALLOWLIST[0] === "*";
893
+ app.use((req, res, next) => localBrowserGuard(req, res, next));
893
894
  app.use((req, res, next) => {
894
895
  const origin = req.get("origin") || "";
895
896
  if (CORS_WILDCARD) {
@@ -907,6 +908,43 @@ app.use((req, res, next) => {
907
908
  // ---------- Auth middleware ----------
908
909
  // No-op in local-only mode (MCP_AUTH_TOKEN unset). Enforces 401 when token is set.
909
910
  const MCP_TOKEN = process.env.MCP_AUTH_TOKEN || "";
911
+
912
+ // ---------- Local-only browser guard (#4226) ----------
913
+ // Binding to loopback does not keep web pages out: any page the user opens
914
+ // can send requests to 127.0.0.1, and a DNS-rebinding page can even make the
915
+ // request look same-origin. CORS only decides whether the page may READ the
916
+ // reply; a "simple" cross-origin POST runs without a preflight. So when there
917
+ // is no MCP_AUTH_TOKEN, refuse requests that a browser sent on behalf of a
918
+ // foreign page:
919
+ // - the Host header must name a loopback address (stops DNS rebinding);
920
+ // - a request carrying an Origin header must come from a loopback origin or
921
+ // from an origin listed explicitly in MCP_CORS_ORIGIN.
922
+ // Native MCP clients (CLI, IDE, curl) send no Origin and a loopback Host.
923
+ const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
924
+ function hostnameOf(value) {
925
+ try {
926
+ return new URL(value.includes("://") ? value : `http://${value}`).hostname.toLowerCase();
927
+ } catch {
928
+ return "";
929
+ }
930
+ }
931
+ function isLoopbackHostname(hostname) {
932
+ return LOOPBACK_HOSTNAMES.has(hostname) || hostname.endsWith(".localhost");
933
+ }
934
+ function localBrowserGuard(req, res, next) {
935
+ if (MCP_TOKEN) return next();
936
+ if (!isLoopbackHostname(hostnameOf(req.get("host") || ""))) {
937
+ return res.status(403).json({ error: "forbidden host" });
938
+ }
939
+ const origin = req.get("origin");
940
+ if (origin !== undefined) {
941
+ const explicitlyAllowed = !CORS_WILDCARD && CORS_ALLOWLIST.includes(origin);
942
+ if (!explicitlyAllowed && !isLoopbackHostname(hostnameOf(origin))) {
943
+ return res.status(403).json({ error: "forbidden origin" });
944
+ }
945
+ }
946
+ next();
947
+ }
910
948
  function requireAuth(req, res, next) {
911
949
  if (req.path === "/health") return next();
912
950
  if (!MCP_TOKEN) return next();
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * #4226 — with MCP_AUTH_TOKEN unset (local-only mode) the bridge must not
4
+ * serve requests a browser sends for a foreign web page, even on loopback.
5
+ * Spawns the bridge and checks over the wire:
6
+ * G1. a native client (no Origin, loopback Host) still reaches POST /mcp
7
+ * G2. POST /mcp with a foreign Origin → 403 (simple request, no preflight)
8
+ * G3. OPTIONS preflight from a foreign Origin → 403, no ACAO: *
9
+ * G4. POST /mcp with a non-loopback Host (DNS rebinding) → 403
10
+ * G5. a loopback Origin (local dev UI) is still accepted
11
+ * G6. an Origin listed in MCP_CORS_ORIGIN is still accepted
12
+ * G7. with MCP_AUTH_TOKEN set the guard is off and bearer auth decides
13
+ *
14
+ * node test-local-browser-guard.mjs
15
+ */
16
+ import { spawn } from "node:child_process";
17
+ import http from "node:http";
18
+ import path from "node:path";
19
+ import { setTimeout as sleep } from "node:timers/promises";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const ENTRY = path.join(path.dirname(fileURLToPath(import.meta.url)), "index.js");
23
+ const QUIET = {
24
+ MCP_GROUP_INTELLIGENCE: "false",
25
+ MCP_GROUP_AGENTS: "false",
26
+ MCP_GROUP_MEMORY: "false",
27
+ MCP_GROUP_DEVTOOLS: "false",
28
+ MCP_GROUP_NEURAL: "false",
29
+ };
30
+ let failed = 0;
31
+ function check(cond, label) {
32
+ console.log(` ${cond ? "✓" : "✗"} ${label}`);
33
+ if (!cond) failed++;
34
+ }
35
+
36
+ async function withBridge(env, fn) {
37
+ const port = 20000 + Math.floor(Math.random() * 10000);
38
+ const child = spawn("node", [ENTRY], { env: { ...process.env, ...QUIET, PORT: String(port), ...env }, stdio: "ignore" });
39
+ try {
40
+ for (let i = 0; i < 50; i++) {
41
+ const up = await request(port, { method: "GET", path: "/health" }).then(r => r.status > 0, () => false);
42
+ if (up) break;
43
+ await sleep(100);
44
+ }
45
+ await fn(port);
46
+ } finally {
47
+ child.kill();
48
+ }
49
+ }
50
+
51
+ // node:http, not fetch: fetch does not let a caller set Host or Origin.
52
+ function request(port, { method = "POST", path: p = "/mcp", headers = {}, body } = {}) {
53
+ return new Promise((resolve, reject) => {
54
+ const req = http.request({ host: "127.0.0.1", port, method, path: p, headers }, (res) => {
55
+ res.resume();
56
+ res.on("end", () => resolve({ status: res.statusCode, headers: res.headers }));
57
+ });
58
+ req.on("error", reject);
59
+ if (body) req.write(body);
60
+ req.end();
61
+ });
62
+ }
63
+
64
+ const initialize = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} });
65
+ const json = { "Content-Type": "application/json" };
66
+
67
+ console.log("local-only mode (no MCP_AUTH_TOKEN)");
68
+ await withBridge({ MCP_CORS_ORIGIN: "" }, async (port) => {
69
+ check((await request(port, { headers: json, body: initialize })).status === 200, "G1 native client reaches /mcp");
70
+ check((await request(port, { headers: { ...json, Origin: "https://example.com" }, body: initialize })).status === 403, "G2 foreign Origin POST → 403");
71
+ const pre = await request(port, { method: "OPTIONS", headers: { Origin: "https://example.com", "Access-Control-Request-Method": "POST" } });
72
+ check(pre.status === 403 && pre.headers["access-control-allow-origin"] === undefined, "G3 foreign preflight → 403 without ACAO");
73
+ check((await request(port, { headers: { ...json, Host: "evil.example" }, body: initialize })).status === 403, "G4 non-loopback Host → 403");
74
+ check((await request(port, { headers: { ...json, Origin: "http://localhost:5173" }, body: initialize })).status === 200, "G5 loopback Origin accepted");
75
+ });
76
+ await withBridge({ MCP_CORS_ORIGIN: "https://ui.example" }, async (port) => {
77
+ check((await request(port, { headers: { ...json, Origin: "https://ui.example" }, body: initialize })).status === 200, "G6 allowlisted Origin accepted");
78
+ });
79
+
80
+ console.log("token mode");
81
+ await withBridge({ MCP_AUTH_TOKEN: "t0ken" }, async (port) => {
82
+ const res = await request(port, { headers: { ...json, Origin: "https://example.com", Authorization: "Bearer t0ken" }, body: initialize });
83
+ check(res.status === 200, "G7 bearer token decides when set");
84
+ });
85
+
86
+ console.log(failed ? `\n${failed} check(s) failed` : "\nall checks passed");
87
+ process.exit(failed ? 1 : 0);