mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(docker): allowlist container-to-host gateway aliases in host guard
An in-container hook curl carries Host: host.docker.internal:<port> (the derived CODEMAN_API_URL), so the always-on host guard must allow host.docker.internal / host.containers.internal or every in-container hook is blocked 403. Exact-match only; not a browser DNS-rebinding surface (resolves to the host only from inside a container netns). Verified end-to-end: quick-start launches claude/shell in a real container with the workspace bind-mounted and hooks scaffolded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,16 @@ export function isLoopbackBindHost(host: string): boolean {
|
|||||||
*/
|
*/
|
||||||
export const DEFAULT_TRUSTED_HOST_SUFFIXES = ['.ts.net', '.trycloudflare.com', '.cfargotunnel.com'];
|
export const DEFAULT_TRUSTED_HOST_SUFFIXES = ['.ts.net', '.trycloudflare.com', '.cfargotunnel.com'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container-to-host gateway aliases (Docker / Podman). A hook `curl` from INSIDE a
|
||||||
|
* docker case carries `Host: host.docker.internal:<port>` (the derived
|
||||||
|
* CODEMAN_API_URL), so the always-on host guard must allow it or every in-container
|
||||||
|
* hook is blocked 403. These names only resolve to the host from within a
|
||||||
|
* container's network namespace, so they are not a DNS-rebinding surface for a
|
||||||
|
* normal browser. Both engines' aliases are allowed so a mixed fleet keeps working.
|
||||||
|
*/
|
||||||
|
export const DOCKER_HOST_GATEWAY_ALIASES = ['host.docker.internal', 'host.containers.internal'];
|
||||||
|
|
||||||
/** Policy inputs for the anti-DNS-rebinding Host allowlist + cross-site Origin guard. */
|
/** Policy inputs for the anti-DNS-rebinding Host allowlist + cross-site Origin guard. */
|
||||||
export interface HostPolicy {
|
export interface HostPolicy {
|
||||||
/** The host the server is bound to (e.g. '127.0.0.1', '0.0.0.0', or a hostname). */
|
/** The host the server is bound to (e.g. '127.0.0.1', '0.0.0.0', or a hostname). */
|
||||||
@@ -98,6 +108,8 @@ function matchesHost(hostname: string, policy: HostPolicy): boolean {
|
|||||||
const bind = parseAuthorityHostname(policy.bindHost);
|
const bind = parseAuthorityHostname(policy.bindHost);
|
||||||
if (bind && hostname === bind) return true;
|
if (bind && hostname === bind) return true;
|
||||||
if (policy.tunnelHost && hostname === policy.tunnelHost) return true;
|
if (policy.tunnelHost && hostname === policy.tunnelHost) return true;
|
||||||
|
// Docker/Podman container-to-host gateway aliases (for in-container hook curls).
|
||||||
|
if (DOCKER_HOST_GATEWAY_ALIASES.includes(hostname)) return true;
|
||||||
for (const suffix of DEFAULT_TRUSTED_HOST_SUFFIXES) {
|
for (const suffix of DEFAULT_TRUSTED_HOST_SUFFIXES) {
|
||||||
if (hostname === suffix.slice(1) || hostname.endsWith(suffix)) return true;
|
if (hostname === suffix.slice(1) || hostname.endsWith(suffix)) return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ describe('isAllowedRequestHost — anti-DNS-rebinding', () => {
|
|||||||
expect(isAllowedRequestHost('eviltrycloudflare.com', loopback)).toBe(false);
|
expect(isAllowedRequestHost('eviltrycloudflare.com', loopback)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts the docker/podman container-to-host gateway aliases (in-container hooks)', () => {
|
||||||
|
expect(isAllowedRequestHost('host.docker.internal:3000', loopback)).toBe(true);
|
||||||
|
expect(isAllowedRequestHost('host.containers.internal:3000', loopback)).toBe(true);
|
||||||
|
// a lookalike is still rejected (exact match only)
|
||||||
|
expect(isAllowedRequestHost('host.docker.internal.evil.com', loopback)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts the configured bind host when it is a hostname', () => {
|
it('accepts the configured bind host when it is a hostname', () => {
|
||||||
const policy: HostPolicy = { bindHost: 'mybox.local', allowedHosts: [], tunnelHost: null };
|
const policy: HostPolicy = { bindHost: 'mybox.local', allowedHosts: [], tunnelHost: null };
|
||||||
expect(isAllowedRequestHost('mybox.local:3000', policy)).toBe(true);
|
expect(isAllowedRequestHost('mybox.local:3000', policy)).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user