mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: harden secret-file readers
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { listIrcAccountIds, resolveDefaultIrcAccountId } from "./accounts.js";
|
||||
import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
function asConfig(value: unknown): CoreConfig {
|
||||
@@ -76,3 +79,28 @@ describe("resolveDefaultIrcAccountId", () => {
|
||||
expect(resolveDefaultIrcAccountId(cfg)).toBe("aaa");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveIrcAccount", () => {
|
||||
it.runIf(process.platform !== "win32")("rejects symlinked password files", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-irc-account-"));
|
||||
const passwordFile = path.join(dir, "password.txt");
|
||||
const passwordLink = path.join(dir, "password-link.txt");
|
||||
fs.writeFileSync(passwordFile, "secret-pass\n", "utf8");
|
||||
fs.symlinkSync(passwordFile, passwordLink);
|
||||
|
||||
const cfg = asConfig({
|
||||
channels: {
|
||||
irc: {
|
||||
host: "irc.example.com",
|
||||
nick: "claw",
|
||||
passwordFile: passwordLink,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const account = resolveIrcAccount({ cfg });
|
||||
expect(account.password).toBe("");
|
||||
expect(account.passwordSource).toBe("none");
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createAccountListHelpers,
|
||||
normalizeResolvedSecretInputString,
|
||||
@@ -100,13 +100,11 @@ function resolvePassword(accountId: string, merged: IrcAccountConfig) {
|
||||
}
|
||||
|
||||
if (merged.passwordFile?.trim()) {
|
||||
try {
|
||||
const filePassword = readFileSync(merged.passwordFile.trim(), "utf-8").trim();
|
||||
if (filePassword) {
|
||||
return { password: filePassword, source: "passwordFile" as const };
|
||||
}
|
||||
} catch {
|
||||
// Ignore unreadable files here; status will still surface missing configuration.
|
||||
const filePassword = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
|
||||
rejectSymlink: true,
|
||||
});
|
||||
if (filePassword) {
|
||||
return { password: filePassword, source: "passwordFile" as const };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,11 +135,10 @@ function resolveNickServConfig(accountId: string, nickserv?: IrcNickServConfig):
|
||||
envPassword ||
|
||||
"";
|
||||
if (!resolvedPassword && passwordFile) {
|
||||
try {
|
||||
resolvedPassword = readFileSync(passwordFile, "utf-8").trim();
|
||||
} catch {
|
||||
// Ignore unreadable files; monitor/probe status will surface failures.
|
||||
}
|
||||
resolvedPassword =
|
||||
tryReadSecretFileSync(passwordFile, "IRC NickServ password file", {
|
||||
rejectSymlink: true,
|
||||
}) ?? "";
|
||||
}
|
||||
|
||||
const merged: IrcNickServConfig = {
|
||||
|
||||
30
extensions/nextcloud-talk/src/accounts.test.ts
Normal file
30
extensions/nextcloud-talk/src/accounts.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveNextcloudTalkAccount } from "./accounts.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
describe("resolveNextcloudTalkAccount", () => {
|
||||
it.runIf(process.platform !== "win32")("rejects symlinked botSecretFile paths", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-nextcloud-talk-"));
|
||||
const secretFile = path.join(dir, "secret.txt");
|
||||
const secretLink = path.join(dir, "secret-link.txt");
|
||||
fs.writeFileSync(secretFile, "bot-secret\n", "utf8");
|
||||
fs.symlinkSync(secretFile, secretLink);
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
"nextcloud-talk": {
|
||||
baseUrl: "https://cloud.example.com",
|
||||
botSecretFile: secretLink,
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const account = resolveNextcloudTalkAccount({ cfg });
|
||||
expect(account.secret).toBe("");
|
||||
expect(account.secretSource).toBe("none");
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createAccountListHelpers,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
@@ -88,13 +88,13 @@ function resolveNextcloudTalkSecret(
|
||||
}
|
||||
|
||||
if (merged.botSecretFile) {
|
||||
try {
|
||||
const fileSecret = readFileSync(merged.botSecretFile, "utf-8").trim();
|
||||
if (fileSecret) {
|
||||
return { secret: fileSecret, source: "secretFile" };
|
||||
}
|
||||
} catch {
|
||||
// File not found or unreadable, fall through.
|
||||
const fileSecret = tryReadSecretFileSync(
|
||||
merged.botSecretFile,
|
||||
"Nextcloud Talk bot secret file",
|
||||
{ rejectSymlink: true },
|
||||
);
|
||||
if (fileSecret) {
|
||||
return { secret: fileSecret, source: "secretFile" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveZaloToken } from "./token.js";
|
||||
import type { ZaloConfig } from "./types.js";
|
||||
@@ -55,4 +58,20 @@ describe("resolveZaloToken", () => {
|
||||
expect(res.token).toBe("work-token");
|
||||
expect(res.source).toBe("config");
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")("rejects symlinked token files", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-zalo-token-"));
|
||||
const tokenFile = path.join(dir, "token.txt");
|
||||
const tokenLink = path.join(dir, "token-link.txt");
|
||||
fs.writeFileSync(tokenFile, "file-token\n", "utf8");
|
||||
fs.symlinkSync(tokenFile, tokenLink);
|
||||
|
||||
const cfg = {
|
||||
tokenFile: tokenLink,
|
||||
} as ZaloConfig;
|
||||
const res = resolveZaloToken(cfg);
|
||||
expect(res.token).toBe("");
|
||||
expect(res.source).toBe("none");
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
|
||||
import type { BaseTokenResolution } from "openclaw/plugin-sdk/zalo";
|
||||
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
|
||||
import type { ZaloConfig } from "./types.js";
|
||||
@@ -9,16 +9,7 @@ export type ZaloTokenResolution = BaseTokenResolution & {
|
||||
};
|
||||
|
||||
function readTokenFromFile(tokenFile: string | undefined): string {
|
||||
const trimmedPath = tokenFile?.trim();
|
||||
if (!trimmedPath) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return readFileSync(trimmedPath, "utf8").trim();
|
||||
} catch {
|
||||
// ignore read failures
|
||||
return "";
|
||||
}
|
||||
return tryReadSecretFileSync(tokenFile, "Zalo token file", { rejectSymlink: true }) ?? "";
|
||||
}
|
||||
|
||||
export function resolveZaloToken(
|
||||
|
||||
Reference in New Issue
Block a user