fix(browser): require auth on control HTTP and auto-bootstrap token

This commit is contained in:
Peter Steinberger
2026-02-13 02:01:57 +01:00
parent 85409e401b
commit 9230a2ae14
11 changed files with 634 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
- Security/Sandbox: confine mirrored skill sync destinations to the sandbox `skills/` root and stop using frontmatter-controlled skill names as filesystem destination paths. Thanks @1seal.
- Security/Web tools: treat browser/web content as untrusted by default (wrapped outputs for browser snapshot/tabs/console and structured external-content metadata for web tools), and strip `toolResult.details` from model-facing transcript/compaction inputs to reduce prompt-injection replay risk.
- Security/Hooks: harden webhook and device token verification with shared constant-time secret comparison, and add per-client auth-failure throttling for hook endpoints (`429` + `Retry-After`). Thanks @akhmittra.
- Security/Browser: require auth for loopback browser control HTTP routes, auto-generate `gateway.auth.token` when browser control starts without auth, and add a security-audit check for unauthenticated browser control. Thanks @tcusolle.
- Sessions/Gateway: harden transcript path resolution and reject unsafe session IDs/file paths so session operations stay within agent sessions directories. Thanks @akhmittra.
- Gateway: raise WS payload/buffer limits so 5,000,000-byte image attachments work reliably. (#14486) Thanks @0xRaini.
- Logging/CLI: use local timezone timestamps for console prefixing, and include `±HH:MM` offsets when using `openclaw logs --local-time` to avoid ambiguity. (#14771) Thanks @0xRaini.

View File

@@ -192,6 +192,7 @@ Notes:
Key ideas:
- Browser control is loopback-only; access flows through the Gateways auth or node pairing.
- If browser control is enabled and no auth is configured, OpenClaw auto-generates `gateway.auth.token` on startup and persists it to config.
- Keep the Gateway and any node hosts on a private network (Tailscale); avoid public exposure.
- Treat remote CDP URLs/tokens as secrets; prefer env vars or a secrets manager.
@@ -315,6 +316,11 @@ For local integrations only, the Gateway exposes a small loopback HTTP API:
All endpoints accept `?profile=<name>`.
If gateway auth is configured, browser HTTP routes require auth too:
- `Authorization: Bearer <gateway token>`
- `x-openclaw-password: <gateway password>` or HTTP Basic auth with that password
### Playwright requirement
Some features (navigate/act/AI snapshot/role snapshot, element screenshots, PDF) require

View File

@@ -0,0 +1,106 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
loadConfig: vi.fn(() => ({
gateway: {
auth: {
token: "loopback-token",
},
},
})),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: mocks.loadConfig,
};
});
vi.mock("./control-service.js", () => ({
createBrowserControlContext: vi.fn(() => ({})),
startBrowserControlServiceFromConfig: vi.fn(async () => ({ ok: true })),
}));
vi.mock("./routes/dispatcher.js", () => ({
createBrowserRouteDispatcher: vi.fn(() => ({
dispatch: vi.fn(async () => ({ status: 200, body: { ok: true } })),
})),
}));
import { fetchBrowserJson } from "./client-fetch.js";
describe("fetchBrowserJson loopback auth", () => {
beforeEach(() => {
vi.restoreAllMocks();
mocks.loadConfig.mockReset();
mocks.loadConfig.mockReturnValue({
gateway: {
auth: {
token: "loopback-token",
},
},
});
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("adds bearer auth for loopback absolute HTTP URLs", async () => {
const fetchMock = vi.fn(
async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
const res = await fetchBrowserJson<{ ok: boolean }>("http://127.0.0.1:18888/");
expect(res.ok).toBe(true);
const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
const headers = new Headers(init?.headers);
expect(headers.get("authorization")).toBe("Bearer loopback-token");
});
it("does not inject auth for non-loopback absolute URLs", async () => {
const fetchMock = vi.fn(
async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
await fetchBrowserJson<{ ok: boolean }>("http://example.com/");
const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
const headers = new Headers(init?.headers);
expect(headers.get("authorization")).toBeNull();
});
it("keeps caller-supplied auth header", async () => {
const fetchMock = vi.fn(
async () =>
new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
vi.stubGlobal("fetch", fetchMock);
await fetchBrowserJson<{ ok: boolean }>("http://localhost:18888/", {
headers: {
Authorization: "Bearer caller-token",
},
});
const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
const headers = new Headers(init?.headers);
expect(headers.get("authorization")).toBe("Bearer caller-token");
});
});

View File

@@ -1,4 +1,6 @@
import { formatCliCommand } from "../cli/command-format.js";
import { loadConfig } from "../config/config.js";
import { resolveBrowserControlAuth } from "./control-auth.js";
import {
createBrowserControlContext,
startBrowserControlServiceFromConfig,
@@ -9,6 +11,42 @@ function isAbsoluteHttp(url: string): boolean {
return /^https?:\/\//i.test(url.trim());
}
function isLoopbackHttpUrl(url: string): boolean {
try {
const host = new URL(url).hostname.trim().toLowerCase();
return host === "127.0.0.1" || host === "localhost" || host === "::1";
} catch {
return false;
}
}
function withLoopbackBrowserAuth(
url: string,
init: (RequestInit & { timeoutMs?: number }) | undefined,
): RequestInit & { timeoutMs?: number } {
const headers = new Headers(init?.headers ?? {});
if (headers.has("authorization") || headers.has("x-openclaw-password")) {
return { ...init, headers };
}
if (!isLoopbackHttpUrl(url)) {
return { ...init, headers };
}
try {
const cfg = loadConfig();
const auth = resolveBrowserControlAuth(cfg);
if (auth.token) {
headers.set("Authorization", `Bearer ${auth.token}`);
} else if (auth.password) {
headers.set("x-openclaw-password", auth.password);
}
} catch {
// ignore config/auth lookup failures and continue without auth headers
}
return { ...init, headers };
}
function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number): Error {
const hint = isAbsoluteHttp(url)
? "If this is a sandboxed session, ensure the sandbox browser is running and try again."
@@ -69,7 +107,8 @@ export async function fetchBrowserJson<T>(
const timeoutMs = init?.timeoutMs ?? 5000;
try {
if (isAbsoluteHttp(url)) {
return await fetchHttpJson<T>(url, { ...init, timeoutMs });
const httpInit = withLoopbackBrowserAuth(url, init);
return await fetchHttpJson<T>(url, { ...httpInit, timeoutMs });
}
const started = await startBrowserControlServiceFromConfig();
if (!started) {

View File

@@ -0,0 +1,123 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
const mocks = vi.hoisted(() => ({
loadConfig: vi.fn<() => OpenClawConfig>(),
writeConfigFile: vi.fn(async (_cfg: OpenClawConfig) => {}),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: mocks.loadConfig,
writeConfigFile: mocks.writeConfigFile,
};
});
import { ensureBrowserControlAuth } from "./control-auth.js";
describe("ensureBrowserControlAuth", () => {
beforeEach(() => {
vi.restoreAllMocks();
mocks.loadConfig.mockReset();
mocks.writeConfigFile.mockReset();
});
it("returns existing auth and skips writes", async () => {
const cfg: OpenClawConfig = {
gateway: {
auth: {
token: "already-set",
},
},
};
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expect(result).toEqual({ auth: { token: "already-set" } });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
});
it("auto-generates and persists a token when auth is missing", async () => {
const cfg: OpenClawConfig = {
browser: {
enabled: true,
},
};
mocks.loadConfig.mockReturnValue({
browser: {
enabled: true,
},
});
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expect(result.generatedToken).toMatch(/^[0-9a-f]{48}$/);
expect(result.auth.token).toBe(result.generatedToken);
expect(mocks.writeConfigFile).toHaveBeenCalledTimes(1);
const persisted = mocks.writeConfigFile.mock.calls[0]?.[0];
expect(persisted?.gateway?.auth?.mode).toBe("token");
expect(persisted?.gateway?.auth?.token).toBe(result.generatedToken);
});
it("skips auto-generation in test env", async () => {
const cfg: OpenClawConfig = {
browser: {
enabled: true,
},
};
const result = await ensureBrowserControlAuth({
cfg,
env: { NODE_ENV: "test" } as NodeJS.ProcessEnv,
});
expect(result).toEqual({ auth: {} });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
});
it("respects explicit password mode", async () => {
const cfg: OpenClawConfig = {
gateway: {
auth: {
mode: "password",
},
},
browser: {
enabled: true,
},
};
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expect(result).toEqual({ auth: {} });
expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
});
it("reuses auth from latest config snapshot", async () => {
const cfg: OpenClawConfig = {
browser: {
enabled: true,
},
};
mocks.loadConfig.mockReturnValue({
gateway: {
auth: {
token: "latest-token",
},
},
browser: {
enabled: true,
},
});
const result = await ensureBrowserControlAuth({ cfg, env: {} as NodeJS.ProcessEnv });
expect(result).toEqual({ auth: { token: "latest-token" } });
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,88 @@
import crypto from "node:crypto";
import type { OpenClawConfig } from "../config/config.js";
import { loadConfig, writeConfigFile } from "../config/config.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
export type BrowserControlAuth = {
token?: string;
password?: string;
};
export function resolveBrowserControlAuth(
cfg: OpenClawConfig | undefined,
env: NodeJS.ProcessEnv = process.env,
): BrowserControlAuth {
const auth = resolveGatewayAuth({
authConfig: cfg?.gateway?.auth,
env,
tailscaleMode: cfg?.gateway?.tailscale?.mode,
});
const token = typeof auth.token === "string" ? auth.token.trim() : "";
const password = typeof auth.password === "string" ? auth.password.trim() : "";
return {
token: token || undefined,
password: password || undefined,
};
}
function shouldAutoGenerateBrowserAuth(env: NodeJS.ProcessEnv): boolean {
const nodeEnv = (env.NODE_ENV ?? "").trim().toLowerCase();
if (nodeEnv === "test") {
return false;
}
const vitest = (env.VITEST ?? "").trim().toLowerCase();
if (vitest && vitest !== "0" && vitest !== "false" && vitest !== "off") {
return false;
}
return true;
}
export async function ensureBrowserControlAuth(params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
}): Promise<{
auth: BrowserControlAuth;
generatedToken?: string;
}> {
const env = params.env ?? process.env;
const auth = resolveBrowserControlAuth(params.cfg, env);
if (auth.token || auth.password) {
return { auth };
}
if (!shouldAutoGenerateBrowserAuth(env)) {
return { auth };
}
// Respect explicit password mode even if currently unset.
if (params.cfg.gateway?.auth?.mode === "password") {
return { auth };
}
// Re-read latest config to avoid racing with concurrent config writers.
const latestCfg = loadConfig();
const latestAuth = resolveBrowserControlAuth(latestCfg, env);
if (latestAuth.token || latestAuth.password) {
return { auth: latestAuth };
}
if (latestCfg.gateway?.auth?.mode === "password") {
return { auth: latestAuth };
}
const generatedToken = crypto.randomBytes(24).toString("hex");
const nextCfg: OpenClawConfig = {
...latestCfg,
gateway: {
...latestCfg.gateway,
auth: {
...latestCfg.gateway?.auth,
mode: "token",
token: generatedToken,
},
},
};
await writeConfigFile(nextCfg);
return {
auth: { token: generatedToken },
generatedToken,
};
}

View File

@@ -1,6 +1,7 @@
import { loadConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveBrowserConfig, resolveProfile } from "./config.js";
import { ensureBrowserControlAuth } from "./control-auth.js";
import { ensureChromeExtensionRelayServer } from "./extension-relay.js";
import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js";
@@ -28,6 +29,14 @@ export async function startBrowserControlServiceFromConfig(): Promise<BrowserSer
if (!resolved.enabled) {
return null;
}
try {
const ensured = await ensureBrowserControlAuth({ cfg });
if (ensured.generatedToken) {
logService.info("No browser auth configured; generated gateway.auth.token automatically.");
}
} catch (err) {
logService.warn(`failed to auto-configure browser auth: ${String(err)}`);
}
state = {
server: null,

View File

@@ -0,0 +1,109 @@
import { createServer, type AddressInfo } from "node:net";
import { fetch as realFetch } from "undici";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let testPort = 0;
let prevGatewayPort: string | undefined;
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => ({
gateway: {
auth: {
token: "browser-control-secret",
},
},
browser: {
enabled: true,
defaultProfile: "openclaw",
profiles: {
openclaw: { cdpPort: testPort + 1, color: "#FF4500" },
},
},
}),
};
});
vi.mock("./routes/index.js", () => ({
registerBrowserRoutes(app: {
get: (
path: string,
handler: (req: unknown, res: { json: (body: unknown) => void }) => void,
) => void;
}) {
app.get("/", (_req, res) => {
res.json({ ok: true });
});
},
}));
vi.mock("./server-context.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./server-context.js")>();
return {
...actual,
createBrowserRouteContext: vi.fn(() => ({
forProfile: vi.fn(() => ({
stopRunningBrowser: vi.fn(async () => {}),
})),
})),
};
});
describe("browser control HTTP auth", () => {
beforeEach(async () => {
prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT;
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;
testPort = addr.port;
await new Promise<void>((resolve) => probe.close(() => resolve()));
process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2);
});
afterEach(async () => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
if (prevGatewayPort === undefined) {
delete process.env.OPENCLAW_GATEWAY_PORT;
} else {
process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort;
}
const { stopBrowserControlServer } = await import("./server.js");
await stopBrowserControlServer();
});
it("requires bearer auth for standalone browser HTTP routes", async () => {
const { startBrowserControlServerFromConfig } = await import("./server.js");
const started = await startBrowserControlServerFromConfig();
expect(started?.port).toBe(testPort);
const base = `http://127.0.0.1:${testPort}`;
const missingAuth = await realFetch(`${base}/`);
expect(missingAuth.status).toBe(401);
expect(await missingAuth.text()).toContain("Unauthorized");
const badAuth = await realFetch(`${base}/`, {
headers: {
Authorization: "Bearer wrong-token",
},
});
expect(badAuth.status).toBe(401);
const ok = await realFetch(`${base}/`, {
headers: {
Authorization: "Bearer browser-control-secret",
},
});
expect(ok.status).toBe(200);
expect((await ok.json()) as { ok: boolean }).toEqual({ ok: true });
});
});

View File

@@ -1,9 +1,11 @@
import type { Server } from "node:http";
import type { IncomingMessage, Server } from "node:http";
import express from "express";
import type { BrowserRouteRegistrar } from "./routes/types.js";
import { loadConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { safeEqualSecret } from "../security/secret-equal.js";
import { resolveBrowserConfig, resolveProfile } from "./config.js";
import { ensureBrowserControlAuth, resolveBrowserControlAuth } from "./control-auth.js";
import { ensureChromeExtensionRelayServer } from "./extension-relay.js";
import { registerBrowserRoutes } from "./routes/index.js";
import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js";
@@ -12,6 +14,67 @@ let state: BrowserServerState | null = null;
const log = createSubsystemLogger("browser");
const logServer = log.child("server");
function firstHeaderValue(value: string | string[] | undefined): string {
return Array.isArray(value) ? (value[0] ?? "") : (value ?? "");
}
function parseBearerToken(authorization: string): string | undefined {
if (!authorization || !authorization.toLowerCase().startsWith("bearer ")) {
return undefined;
}
const token = authorization.slice(7).trim();
return token || undefined;
}
function parseBasicPassword(authorization: string): string | undefined {
if (!authorization || !authorization.toLowerCase().startsWith("basic ")) {
return undefined;
}
const encoded = authorization.slice(6).trim();
if (!encoded) {
return undefined;
}
try {
const decoded = Buffer.from(encoded, "base64").toString("utf8");
const sep = decoded.indexOf(":");
if (sep < 0) {
return undefined;
}
const password = decoded.slice(sep + 1).trim();
return password || undefined;
} catch {
return undefined;
}
}
function isAuthorizedBrowserRequest(
req: IncomingMessage,
auth: { token?: string; password?: string },
): boolean {
const authorization = firstHeaderValue(req.headers.authorization).trim();
if (auth.token) {
const bearer = parseBearerToken(authorization);
if (bearer && safeEqualSecret(bearer, auth.token)) {
return true;
}
}
if (auth.password) {
const passwordHeader = firstHeaderValue(req.headers["x-openclaw-password"]).trim();
if (passwordHeader && safeEqualSecret(passwordHeader, auth.password)) {
return true;
}
const basicPassword = parseBasicPassword(authorization);
if (basicPassword && safeEqualSecret(basicPassword, auth.password)) {
return true;
}
}
return false;
}
export async function startBrowserControlServerFromConfig(): Promise<BrowserServerState | null> {
if (state) {
return state;
@@ -23,6 +86,17 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
return null;
}
let browserAuth = resolveBrowserControlAuth(cfg);
try {
const ensured = await ensureBrowserControlAuth({ cfg });
browserAuth = ensured.auth;
if (ensured.generatedToken) {
logServer.info("No browser auth configured; generated gateway.auth.token automatically.");
}
} catch (err) {
logServer.warn(`failed to auto-configure browser auth: ${String(err)}`);
}
const app = express();
app.use((req, res, next) => {
const ctrl = new AbortController();
@@ -39,6 +113,15 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
});
app.use(express.json({ limit: "1mb" }));
if (browserAuth.token || browserAuth.password) {
app.use((req, res, next) => {
if (isAuthorizedBrowserRequest(req, browserAuth)) {
return next();
}
res.status(401).send("Unauthorized");
});
}
const ctx = createBrowserRouteContext({
getState: () => state,
});
@@ -76,7 +159,8 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
});
}
logServer.info(`Browser control listening on http://127.0.0.1:${port}/`);
const authMode = browserAuth.token ? "token" : browserAuth.password ? "password" : "off";
logServer.info(`Browser control listening on http://127.0.0.1:${port}/ (auth=${authMode})`);
return state;
}

View File

@@ -287,6 +287,52 @@ describe("security audit", () => {
);
});
it("flags browser control without auth when browser is enabled", async () => {
const cfg: OpenClawConfig = {
gateway: {
controlUi: { enabled: false },
auth: {},
},
browser: {
enabled: true,
},
};
const res = await runSecurityAudit({
config: cfg,
env: {},
includeFilesystem: false,
includeChannelSecurity: false,
});
expect(res.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({ checkId: "browser.control_no_auth", severity: "critical" }),
]),
);
});
it("does not flag browser control auth when gateway token is configured", async () => {
const cfg: OpenClawConfig = {
gateway: {
controlUi: { enabled: false },
auth: { token: "very-long-browser-token-0123456789" },
},
browser: {
enabled: true,
},
};
const res = await runSecurityAudit({
config: cfg,
env: {},
includeFilesystem: false,
includeChannelSecurity: false,
});
expect(res.findings.some((f) => f.checkId === "browser.control_no_auth")).toBe(false);
});
it("warns when remote CDP uses HTTP", async () => {
const cfg: OpenClawConfig = {
browser: {

View File

@@ -2,6 +2,7 @@ import type { ChannelId } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import type { ExecFn } from "./windows-acl.js";
import { resolveBrowserConfig, resolveProfile } from "../browser/config.js";
import { resolveBrowserControlAuth } from "../browser/control-auth.js";
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
import { listChannelPlugins } from "../channels/plugins/index.js";
import { formatCliCommand } from "../cli/command-format.js";
@@ -364,7 +365,10 @@ function collectGatewayConfigFindings(
return findings;
}
function collectBrowserControlFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
function collectBrowserControlFindings(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv,
): SecurityAuditFinding[] {
const findings: SecurityAuditFinding[] = [];
let resolved: ReturnType<typeof resolveBrowserConfig>;
@@ -385,6 +389,20 @@ function collectBrowserControlFindings(cfg: OpenClawConfig): SecurityAuditFindin
return findings;
}
const browserAuth = resolveBrowserControlAuth(cfg, env);
if (!browserAuth.token && !browserAuth.password) {
findings.push({
checkId: "browser.control_no_auth",
severity: "critical",
title: "Browser control has no auth",
detail:
"Browser control HTTP routes are enabled but no gateway.auth token/password is configured. " +
"Any local process (or SSRF to loopback) can call browser control endpoints.",
remediation:
"Set gateway.auth.token (recommended) or gateway.auth.password so browser control HTTP routes require authentication. Restarting the gateway will auto-generate gateway.auth.token when browser control is enabled.",
});
}
for (const name of Object.keys(resolved.profiles)) {
const profile = resolveProfile(resolved, name);
if (!profile || profile.cdpIsLoopback) {
@@ -924,7 +942,7 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise<Secu
findings.push(...collectSyncedFolderFindings({ stateDir, configPath }));
findings.push(...collectGatewayConfigFindings(cfg, env));
findings.push(...collectBrowserControlFindings(cfg));
findings.push(...collectBrowserControlFindings(cfg, env));
findings.push(...collectLoggingFindings(cfg));
findings.push(...collectElevatedFindings(cfg));
findings.push(...collectHooksHardeningFindings(cfg));