fix(web): only use a public address on a fixed listener port (#1862)
Addresses Copilot review round 2. A reverse proxy routes a public address to one stable port, so neither MCP_SANDBOX_FULL_ADDRESS nor MCP_APP_ORIGIN_FULL_ADDRESS is used when the listener's port is OS-assigned: an explicit 0, a config-time collision resolved to 0, or the EADDRINUSE fallback. Validation warnings also stop echoing the URL path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HzuzRPJv7M8wGe3wyw5sSg Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
1dcb112987
commit
925461a043
3 changed files with 110 additions and 22 deletions
|
|
@ -334,8 +334,10 @@ export function createAppOriginController(
|
|||
* (#1862) when it is still safe to use, else the bind-derived one.
|
||||
*
|
||||
* Two reasons to refuse it here rather than only at config time:
|
||||
* - After the EADDRINUSE fallback the reverse proxy still routes the
|
||||
* public origin to the PINNED port, which someone else holds.
|
||||
* - A reverse proxy routes the public origin to one stable port, so it
|
||||
* is only usable on a FIXED port: not an explicit `0`, not a
|
||||
* config-time collision resolved to `0`, and not the EADDRINUSE
|
||||
* fallback, where someone else holds the pinned port.
|
||||
* - It must differ from every trusted ancestor — the sandbox proxy and
|
||||
* the Inspector page, i.e. `embedderOrigins`. The frame is granted
|
||||
* `allow-same-origin` on this path, so sharing either origin hands
|
||||
|
|
@ -345,10 +347,10 @@ export function createAppOriginController(
|
|||
*/
|
||||
const adoptedOrigin = (derived: string): string => {
|
||||
if (!publicOrigin) return derived;
|
||||
if (retriedDynamic) {
|
||||
if (port === 0 || retriedDynamic) {
|
||||
console.warn(
|
||||
`App origin: not using MCP_APP_ORIGIN_FULL_ADDRESS, since it routes to port ${port}, ` +
|
||||
`which is taken; serving app documents from ${derived} instead.`,
|
||||
`App origin: not using MCP_APP_ORIGIN_FULL_ADDRESS, since the listener is not on a fixed ` +
|
||||
`port for it to route to; serving app documents from ${derived} instead.`,
|
||||
);
|
||||
return derived;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,14 +94,15 @@ function parsePublicUrl(raw: string): Parsed {
|
|||
|
||||
/**
|
||||
* The value as it may appear in a log line. Never the raw string: the values
|
||||
* refused for carrying credentials or a query are exactly the ones likely to
|
||||
* hold a secret, and echoing them would leak what the check refused. Userinfo,
|
||||
* query and fragment are dropped; an unparseable value is not echoed at all.
|
||||
* refused for carrying credentials, a query or a path are exactly the ones
|
||||
* likely to hold a secret, and echoing them would leak what the check refused.
|
||||
* Only scheme and host are shown — that is enough to identify which address was
|
||||
* refused — and an unparseable value is not echoed at all.
|
||||
*/
|
||||
export function describeForLog(raw: string): string {
|
||||
try {
|
||||
const u = new URL(raw);
|
||||
return `"${u.protocol}${u.host ? `//${u.host}` : ""}${u.pathname}"`;
|
||||
return `"${u.protocol}${u.host ? `//${u.host}` : ""}"`;
|
||||
} catch {
|
||||
return "(unparseable value, not echoed)";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -341,17 +341,20 @@ export function createSandboxController(
|
|||
const derivedUrl = `http://${urlHost}:${actualPort}/sandbox`;
|
||||
// An operator-supplied public URL (#1862) replaces the derived one
|
||||
// only now that the listener it routes to is actually up — and only
|
||||
// on the port it was configured for. After the EADDRINUSE fallback
|
||||
// the proxy still routes the public URL to the PINNED port, which
|
||||
// someone else holds, so advertising it would send the browser to
|
||||
// the wrong listener. The derived URL at least works locally.
|
||||
if (publicUrl && retriedDynamic) {
|
||||
// when that listener is on a FIXED port. A reverse proxy routes the
|
||||
// public URL to one stable port; an OS-assigned port (an explicit
|
||||
// `0`, a config-time collision resolved to `0`, or the EADDRINUSE
|
||||
// fallback, where someone else holds the pinned port) is not it, so
|
||||
// advertising the public URL would send the browser to the wrong
|
||||
// listener or none. The derived URL at least works locally.
|
||||
const onFixedPort = port !== 0 && !retriedDynamic;
|
||||
if (publicUrl && !onFixedPort) {
|
||||
console.warn(
|
||||
`Sandbox: not advertising MCP_SANDBOX_FULL_ADDRESS, since it routes to port ${port}, ` +
|
||||
`which is taken; advertising ${derivedUrl} instead.`,
|
||||
`Sandbox: not advertising MCP_SANDBOX_FULL_ADDRESS, since the sandbox is not on a fixed ` +
|
||||
`port for it to route to; advertising ${derivedUrl} instead.`,
|
||||
);
|
||||
}
|
||||
sandboxUrl = publicUrl && !retriedDynamic ? publicUrl : derivedUrl;
|
||||
sandboxUrl = publicUrl && onFixedPort ? publicUrl : derivedUrl;
|
||||
boundPort = actualPort;
|
||||
settle({ port: actualPort, url: sandboxUrl });
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue