diff --git a/docs/ida-provider.md b/docs/ida-provider.md index 03362fec164..4eb9f9ab189 100644 --- a/docs/ida-provider.md +++ b/docs/ida-provider.md @@ -51,13 +51,19 @@ was established; failed registrations remain unavailable. Correct launch environment. Restart/reconnect that server after changing its environment; an export in another shell does not update a running server. +An HTTP registration's `url` must use `http` or `https` without URL credentials, +and its host must be `localhost`, `127.0.0.1`, `[::1]`, or the Docker/Podman +host-gateway name `host.docker.internal` or `host.containers.internal`. Use a +gateway name when REA runs in a container and IDA MCP listens on the container +host. REA refuses every other host. + Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows. Attached profiles, including native mode, refuse universal Mach-O inputs at admission: every slice shares the recorded whole-file hash, so REA cannot verify which slice the GUI loaded. Open a thin file extracted with `lipo -thin` in both IDA and REA, or use headless mode, which analyzes the selected slice. REA's close operation releases its MCP connection or proxy. It never saves or closes the existing GUI database. External edits remain visible because results are live and are not replayed from snapshots. These checks do not lock the GUI or prove that the database is unpatched. ## Optional native IDA MCP bridge -IDA Pro and IDA Free can use the separate [ida-free-mcp native plugin](https://github.com/0xshlomil/ida-free-mcp) through an explicit attached registration. Install and start that plugin using its upstream instructions, open the original binary in the IDA GUI, and wait for analysis. The native MCP 1.1.2 release binds `0.0.0.0` without a listen-address setting. Restrict inbound access with the host firewall or use an upstream build that binds only to loopback before starting it. REA's loopback URL restriction does not change the plugin's listener. REA does not install IDA, build the plugin, activate a license, or launch the GUI. +IDA Pro and IDA Free can use the separate [ida-free-mcp native plugin](https://github.com/0xshlomil/ida-free-mcp) through an explicit attached registration. Install and start that plugin using its upstream instructions, open the original binary in the IDA GUI, and wait for analysis. The native MCP 1.1.2 release binds `0.0.0.0` without a listen-address setting. Restrict inbound access with the host firewall or use an upstream build that binds only to loopback before starting it. REA's local URL restriction does not change the plugin's listener. REA does not install IDA, build the plugin, activate a license, or launch the GUI. ```json { diff --git a/src/ida/IdaConfiguration.test.ts b/src/ida/IdaConfiguration.test.ts index da624fee0c9..92317c2e5fb 100644 --- a/src/ida/IdaConfiguration.test.ts +++ b/src/ida/IdaConfiguration.test.ts @@ -48,6 +48,9 @@ describe("IDA registration and MCP result boundaries", () => { for (const input of [ { url: "https://example.com/mcp" }, { url: "http://token@127.0.0.1/mcp" }, + { url: "http://token@host.docker.internal:13337/mcp" }, + { url: "http://host.docker.internal.example.com/mcp" }, + { url: "ftp://host.docker.internal/mcp" }, { command: "python", mode: "force_gui" }, { command: "python", workspaceRoot: "relative" }, { command: "python", args: "server.py" }, @@ -64,6 +67,13 @@ describe("IDA registration and MCP result boundaries", () => { }), ); expect((await read(path)).ok).toBe(true); + for (const url of [ + "http://host.docker.internal:13337/mcp", + "http://HOST.CONTAINERS.INTERNAL:13337/mcp", + ]) { + await writeFile(path, JSON.stringify({ url })); + expect(await read(path)).toMatchObject({ ok: true, value: { url } }); + } await writeFile(path, "malformed"); expect((await read(path)).ok).toBe(false); expect((await read(join(root, "absent"))).ok).toBe(false); diff --git a/src/ida/IdaConfiguration.ts b/src/ida/IdaConfiguration.ts index 6d8ffeb47f8..6e5966236d1 100644 --- a/src/ida/IdaConfiguration.ts +++ b/src/ida/IdaConfiguration.ts @@ -25,6 +25,14 @@ export const DEFAULT_IDA_REQUEST_TIMEOUT_MS = 300_000; const MIN_IDA_REQUEST_TIMEOUT_MS = 1; /** Node timer ceiling. Larger registration values would be clamped instead of honored. */ export const MAX_IDA_REQUEST_TIMEOUT_MS = 2_147_483_647; +/** Loopback names plus the Docker and Podman gateway names for the container's host. */ +const LOCAL_IDA_HTTP_HOSTS: ReadonlySet = new Set([ + "localhost", + "127.0.0.1", + "[::1]", + "host.docker.internal", + "host.containers.internal", +]); const common = { sensitive: z @@ -64,7 +72,7 @@ const registrationSchema = z const url = new URL(value); return ( ["http:", "https:"].includes(url.protocol) && - ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) && + LOCAL_IDA_HTTP_HOSTS.has(url.hostname) && url.username === "" && url.password === "" );