fix(security): harden feishu and zalo webhook ingress

This commit is contained in:
Peter Steinberger
2026-02-19 13:30:51 +01:00
parent e0aaf2d399
commit a23e0d5140
4 changed files with 235 additions and 10 deletions

View File

@@ -127,6 +127,7 @@ export const FeishuAccountConfigSchema = z
domain: FeishuDomainSchema.optional(),
connectionMode: FeishuConnectionModeSchema.optional(),
webhookPath: z.string().optional(),
webhookHost: z.string().optional(),
webhookPort: z.number().int().positive().optional(),
capabilities: z.array(z.string()).optional(),
markdown: MarkdownConfigSchema,
@@ -162,6 +163,7 @@ export const FeishuConfigSchema = z
domain: FeishuDomainSchema.optional().default("feishu"),
connectionMode: FeishuConnectionModeSchema.optional().default("websocket"),
webhookPath: z.string().optional().default("/feishu/events"),
webhookHost: z.string().optional(),
webhookPort: z.number().int().positive().optional(),
capabilities: z.array(z.string()).optional(),
markdown: MarkdownConfigSchema,
@@ -191,6 +193,38 @@ export const FeishuConfigSchema = z
})
.strict()
.superRefine((value, ctx) => {
const defaultConnectionMode = value.connectionMode ?? "websocket";
const defaultVerificationToken = value.verificationToken?.trim();
if (defaultConnectionMode === "webhook" && !defaultVerificationToken) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["verificationToken"],
message:
'channels.feishu.connectionMode="webhook" requires channels.feishu.verificationToken',
});
}
for (const [accountId, account] of Object.entries(value.accounts ?? {})) {
if (!account) {
continue;
}
const accountConnectionMode = account.connectionMode ?? defaultConnectionMode;
if (accountConnectionMode !== "webhook") {
continue;
}
const accountVerificationToken =
account.verificationToken?.trim() || defaultVerificationToken;
if (!accountVerificationToken) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["accounts", accountId, "verificationToken"],
message:
`channels.feishu.accounts.${accountId}.connectionMode="webhook" requires ` +
"a verificationToken (account-level or top-level)",
});
}
}
if (value.dmPolicy === "open") {
const allowFrom = value.allowFrom ?? [];
const hasWildcard = allowFrom.some((entry) => String(entry).trim() === "*");