
Cloud Vulnerability DB
Eine von der Community geführte Datenbank für Schwachstellen
With TINA4_DEBUG=true and the server bound past loopback (for example TINA4_HOST=0.0.0.0), a machine on the network can open the /__dev_reload WebSocket by sending Host: localhost. The same machine gets 403 from every /__dev HTTP endpoint. Once the socket is open, every reload the developer triggers sends {type, file, mtime} to it, so a remote listener sees the path of each file being edited.
The default debug bind is 127.0.0.1, so this needs an explicit non-loopback bind. The remote caller cannot trigger a reload or read file contents, because the HTTP endpoints stay gated.
The upgrade check for /__dev* applies the Host allow-list (ADR-0082) but not the raw socket-peer check that the /__dev HTTP gate applies: packages/core/src/server.ts, the server.on("upgrade") handler. The HTTP gate also calls devPeerAllowed (packages/core/src/devAdmin.ts), and the socket gate does not.
9559385)TINA4_DEBUG=true TINA4_HOST=0.0.0.0./__dev_reload with Host: localhost. The server answers 101 Switching Protocols. GET /__dev/api/mtime from the same machine answers 403.POST /__dev/api/reload with {"file":"src/routes/secret-leak.php","type":"reload"}. The remote socket receives that path.Apply the same raw-peer check (loopback, plus the TINA4_DEV_ALLOWED_PEERS opt-in) before accepting the upgrade. This matches the documented behaviour: a peer that the gate refuses is already not given the toolbar, so it never had a reload client.
Verified on 9559385:
403.101 and still receives the reload.TINA4_DEV_ALLOWED_PEERS=<lan ip>, the remote socket opens again.diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts
index 1d02d5d..ea95ea0 100644
--- a/packages/core/src/server.ts
+++ b/packages/core/src/server.ts
@@ -48,7 +48,7 @@ import { generateNonce, runWithCspNonce, currentCspNonce } from "./csp.js";
import { createHealthRoutes } from "./health.js";
import { rateLimiter } from "./rateLimiter.js";
import { Log } from "./logger.js";
-import { DevAdmin, RequestInspector, WsTracker, devRequestDenial, devHostAllowed, devToolbarAllowed } from "./devAdmin.js";
+import { DevAdmin, RequestInspector, WsTracker, devRequestDenial, devHostAllowed, devPeerAllowed, devToolbarAllowed } from "./devAdmin.js";
import { CLOSE_GOING_AWAY, devReloadWs, serveWebSocketRoute, wsRouteManager } from "./websocket.js";
import { feedbackEnabled, injectFeedbackWidget } from "./feedback.js";
import { I18n } from "./i18n.js";
@@ -2320,8 +2320,9 @@ export async function startServer(config?: Tina4Config): Promise<{
const upPath = (req.url ?? "/").split("?")[0];
// Dev-reload channel (debug only).
if (isDevMode() && upPath === "/__dev_reload") {
- // ADR-0082: the reload socket answers only a loopback / TINA4_HOST Host.
- if (!devHostAllowed(req.headers.host)) {
+ // ADR-0082: the reload socket answers only a loopback / TINA4_HOST Host,
+ // from a raw socket peer the /__dev HTTP gate would also let in.
+ if (!devHostAllowed(req.headers.host) || !devPeerAllowed(socket.remoteAddress)) {
try { socket.write("HTTP/1.1 403 Forbidden\r\n\r\n"); socket.destroy(); } catch { /* gone */ }
return;
}
diff --git a/test/devSurfaceContract.test.ts b/test/devSurfaceContract.test.ts
index 984c0b8..40c948f 100644
--- a/test/devSurfaceContract.test.ts
+++ b/test/devSurfaceContract.test.ts
@@ -79,9 +79,9 @@ const get = (port: number, path: string, headers: Record<string, string> = {}) =
const post = (port: number, path: string, body: unknown, headers: Record<string, string> = {}) => request("POST", port, path, body, headers);
/** A raw socket request, so a path like `/../x` reaches the server verbatim. */
-function rawRequest(port: number, head: string): Promise<string> {
+function rawRequest(port: number, head: string, peer = "127.0.0.1"): Promise<string> {
return new Promise((resolve, reject) => {
- const socket = net.connect(port, "127.0.0.1", () => socket.write(head));
+ const socket = net.connect(port, peer, () => socket.write(head));
let raw = "";
socket.setTimeout(3000, () => socket.destroy());
socket.on("data", (c) => {
@@ -92,9 +92,9 @@ function rawRequest(port: number, head: string): Promise<string> {
socket.on("error", reject);
});
}
-const wsUpgrade = (port: number, host: string) => rawRequest(port,
+const wsUpgrade = (port: number, host: string, peer = "127.0.0.1") => rawRequest(port,
`GET /__dev_reload HTTP/1.1\r\nHost: ${host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n` +
- `Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n`);
+ `Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n`, peer);
// ── throwaway project + a sibling that shares its name as a prefix ───────────
mkdirSync(join(TEST_DIR, "src/routes"), { recursive: true });
@@ -374,6 +374,26 @@ const port = (peerServer.address() as any).port;
`dash=${dashIn.status} page-has-toolbar=${pageIn.raw.includes("tina4-dev-toolbar")}`);
}
+// ── the reload socket checks the raw peer, not only the Host header ─────────
+// Last: a second bound server changes shared state the checks above rely on.
+{
+ // The Host header is the caller's to choose; the raw socket peer is not. A
+ // server bound past loopback, so a real LAN peer can reach the upgrade.
+ const lanPort = await freePort();
+ const lanBoot = await startServer({
+ port: lanPort, host: "0.0.0.0", routesDir: join(TEST_DIR, "src/routes"),
+ modelsDir: join(TEST_DIR, "src/models"), staticDir: join(TEST_DIR, "public"),
+ });
+ const remote = await wsUpgrade(lanPort, `localhost:${lanPort}`, LAN_IP!).catch((e) => String(e));
+ setEnv({ TINA4_DEV_ALLOWED_PEERS: LAN_IP! });
+ const allowed = await wsUpgrade(lanPort, `localhost:${lanPort}`, LAN_IP!).catch((e) => String(e));
+ setEnv({ TINA4_DEV_ALLOWED_PEERS: undefined });
+ lanBoot.close();
+ assert("a remote peer cannot open the reload socket by claiming localhost",
+ remote.startsWith("HTTP/1.1 403") && allowed.startsWith("HTTP/1.1 101"),
+ `remote=${JSON.stringify(remote.split("\r\n")[0])} allowed=${JSON.stringify(allowed.split("\r\n")[0])}`);
+}
+
// ── cleanup — reap everything we spawned ────────────────────────────────────
peerServer.close();
boot.close();Quelle: NVD
Kostenlose Verwundbarkeitsbewertung
Bewerten Sie Ihre Cloudsicherheitspraktiken in 9 Sicherheitsbereichen, um Ihr Risikoniveau zu bewerten und Schwachstellen in Ihren Verteidigungen zu identifizieren.
Eine personalisierte Demo anfordern
"Die beste Benutzererfahrung, die ich je gesehen habe, bietet vollständige Transparenz für Cloud-Workloads."
"„Wiz bietet eine zentrale Oberfläche, um zu sehen, was in unseren Cloud-Umgebungen vor sich geht.“ "
"„Wir wissen, dass, wenn Wiz etwas als kritisch identifiziert, es auch wirklich kritisch ist.“"