fix(qqbot): avoid memory spikes from oversized client secret files (#110002)

* fix(qqbot): reject oversized client secret files

* test(qqbot): derive oversized fixture from shared limit

* test(qqbot): track credential fixtures for cleanup

* fix(qqbot): keep tests inside plugin boundary

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
wahaha1223
2026-07-20 19:00:28 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent 1e3b76f26b
commit e1ca392dfb
2 changed files with 56 additions and 3 deletions
+12 -3
View File
@@ -1,7 +1,7 @@
// Qqbot helper module supports config behavior.
import fs from "node:fs";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
import { coerceSecretRef, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
import { getPlatformAdapter } from "../engine/adapter/index.js";
import {
@@ -135,8 +135,17 @@ export function resolveQQBotAccount(
secretSource = "config";
} else if (accountConfig.clientSecretFile) {
try {
clientSecret = fs.readFileSync(accountConfig.clientSecretFile, "utf8").trim();
secretSource = "file";
const fileSecret = tryReadSecretFileSync(
accountConfig.clientSecretFile,
"QQ Bot client secret",
// Existing clientSecretFile paths may be symlinks or hardlinks. Keep
// that contract while gaining the shared credential size limit.
{ rejectHardlinks: false },
);
if (fileSecret) {
clientSecret = fileSecret;
secretSource = "file";
}
} catch {
secretSource = "none";
}
+44
View File
@@ -1,10 +1,13 @@
// Qqbot tests cover config plugin behavior.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
type JsonSchemaObject,
validateJsonSchemaValue,
} from "openclaw/plugin-sdk/json-schema-runtime";
import { DEFAULT_SECRET_FILE_MAX_BYTES } from "openclaw/plugin-sdk/secret-file-runtime";
import { describe, expect, it } from "vitest";
import { qqbotSetupAdapterShared } from "./bridge/config-shared.js";
import {
@@ -240,6 +243,47 @@ describe("qqbot config", () => {
expect(resolved.name).toBe("Bot Two");
});
it("rejects oversized client secret files", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-client-secret-"));
try {
const secretFile = path.join(tempDir, "secret");
fs.writeFileSync(secretFile, "x".repeat(DEFAULT_SECRET_FILE_MAX_BYTES + 1));
const resolved = resolveQQBotAccount({
channels: { qqbot: { appId: "123456", clientSecretFile: secretFile } },
} as OpenClawConfig);
expect(resolved.clientSecret).toBe("");
expect(resolved.secretSource).toBe("none");
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});
it.runIf(process.platform !== "win32").each(["symlink", "hardlink"] as const)(
"continues to resolve client secret files through a %s",
(linkType) => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-client-secret-link-"));
try {
const targetFile = path.join(tempDir, "secret-target");
const linkedFile = path.join(tempDir, "secret-link");
fs.writeFileSync(targetFile, " fixture-secret\n");
if (linkType === "symlink") {
fs.symlinkSync(targetFile, linkedFile);
} else {
fs.linkSync(targetFile, linkedFile);
}
const resolved = resolveQQBotAccount({
channels: { qqbot: { appId: "123456", clientSecretFile: linkedFile } },
} as OpenClawConfig);
expect(resolved.clientSecret).toBe("fixture-secret");
expect(resolved.secretSource).toBe("file");
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
},
);
it("resolves env SecretRefs on runtime resolution", () => {
const cfg = makeQqbotSecretRefConfig();
const previous = process.env.QQBOT_CLIENT_SECRET;