fix(browser): fail closed browser auth bootstrap

This commit is contained in:
Agent
2026-03-01 21:39:39 +00:00
parent 3a93a7bb1e
commit e4d22fb07a
3 changed files with 104 additions and 0 deletions

View File

@@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai
- CLI/Startup (Raspberry Pi + small hosts): speed up startup by avoiding unnecessary plugin preload on fast routes, adding root `--version` fast-path bootstrap bypass, parallelizing status JSON/non-JSON scans where safe, and enabling Node compile cache at startup with env override compatibility (`NODE_COMPILE_CACHE`, `NODE_DISABLE_COMPILE_CACHE`). (#5871) Thanks @BookCatKid and @vincentkoc for raising startup reports, and @lupuletic for related startup work in #27973.
- Telegram/Outbound API proxy env: keep the Node 22 `autoSelectFamily` global-dispatcher workaround while restoring env-proxy support by using `EnvHttpProxyAgent` so `HTTP_PROXY`/`HTTPS_PROXY` continue to apply to outbound requests. (#26207) Thanks @qsysbio-cjw for reporting and @rylena and @vincentkoc for work.
- Browser/Security: fail closed on browser-control auth bootstrap errors; if auto-auth setup fails and no explicit token/password exists, browser control server startup now aborts instead of starting unauthenticated. This ships in the next npm release. Thanks @ijxpwastaken.
- Docs/Slack manifest scopes: add missing DM/group-DM bot scopes (`im:read`, `im:write`, `mpim:read`, `mpim:write`) to the Slack app manifest example so DM setup guidance is complete. (#29999) Thanks @JcMinarro.
- Slack/Onboarding token help: update setup text to include the “From manifest” app-creation path and current install wording for obtaining the `xoxb-` bot token. (#30846) Thanks @yzhong52.
- Slack/Bot attachment-only messages: when `allowBots: true`, bot messages with empty `text` now include non-forwarded attachment `text`/`fallback` content so webhook alerts are not silently dropped. (#27616)

View File

@@ -0,0 +1,92 @@
import { createServer, type AddressInfo } from "node:net";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
controlPort: 0,
ensureBrowserControlAuth: vi.fn(async () => {
throw new Error("read-only config");
}),
resolveBrowserControlAuth: vi.fn(() => ({})),
ensureExtensionRelayForProfiles: vi.fn(async () => {}),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => ({
browser: {
enabled: true,
},
}),
};
});
vi.mock("./config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./config.js")>();
return {
...actual,
resolveBrowserConfig: vi.fn(() => ({
enabled: true,
controlPort: mocks.controlPort,
})),
};
});
vi.mock("./control-auth.js", () => ({
ensureBrowserControlAuth: mocks.ensureBrowserControlAuth,
resolveBrowserControlAuth: mocks.resolveBrowserControlAuth,
}));
vi.mock("./routes/index.js", () => ({
registerBrowserRoutes: vi.fn(() => {}),
}));
vi.mock("./server-context.js", () => ({
createBrowserRouteContext: vi.fn(() => ({})),
}));
vi.mock("./server-lifecycle.js", () => ({
ensureExtensionRelayForProfiles: mocks.ensureExtensionRelayForProfiles,
stopKnownBrowserProfiles: vi.fn(async () => {}),
}));
vi.mock("./pw-ai-state.js", () => ({
isPwAiLoaded: vi.fn(() => false),
}));
const { startBrowserControlServerFromConfig, stopBrowserControlServer } =
await import("./server.js");
async function getFreePort(): Promise<number> {
const probe = createServer();
await new Promise<void>((resolve, reject) => {
probe.once("error", reject);
probe.listen(0, "127.0.0.1", () => resolve());
});
const addr = probe.address() as AddressInfo;
await new Promise<void>((resolve) => probe.close(() => resolve()));
return addr.port;
}
describe("browser control auth bootstrap failures", () => {
beforeEach(async () => {
mocks.controlPort = await getFreePort();
mocks.ensureBrowserControlAuth.mockClear();
mocks.resolveBrowserControlAuth.mockClear();
mocks.ensureExtensionRelayForProfiles.mockClear();
});
afterEach(async () => {
await stopBrowserControlServer();
});
it("fails closed when auth bootstrap throws and no auth is configured", async () => {
const started = await startBrowserControlServerFromConfig();
expect(started).toBeNull();
expect(mocks.ensureBrowserControlAuth).toHaveBeenCalledTimes(1);
expect(mocks.resolveBrowserControlAuth).toHaveBeenCalledTimes(1);
expect(mocks.ensureExtensionRelayForProfiles).not.toHaveBeenCalled();
});
});

View File

@@ -30,6 +30,7 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
}
let browserAuth = resolveBrowserControlAuth(cfg);
let browserAuthBootstrapFailed = false;
try {
const ensured = await ensureBrowserControlAuth({ cfg });
browserAuth = ensured.auth;
@@ -38,6 +39,16 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
}
} catch (err) {
logServer.warn(`failed to auto-configure browser auth: ${String(err)}`);
browserAuthBootstrapFailed = true;
}
// Fail closed: if auth bootstrap failed and no explicit auth is available,
// do not start the browser control HTTP server.
if (browserAuthBootstrapFailed && !browserAuth.token && !browserAuth.password) {
logServer.error(
"browser control startup aborted: authentication bootstrap failed and no fallback auth is configured.",
);
return null;
}
const app = express();