fix(housekeeping): harden command dispatch boundaries
This commit is contained in:
1 parent
796d009c07
commit
00618fb2a3
8 files changed
+608
-30
No files matched your search
@@ -504,7 +504,7 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
it.each([
|
||||
null,
|
||||
[],
|
||||
Object.assign(Object.create({ inherited: true }), {
|
||||
Object.assign(Object.create(Object.freeze({})), {
|
||||
commandId: "system.dispatch.unknown",
|
||||
input: {},
|
||||
}),
|
||||
@@ -691,4 +691,196 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
expect(JSON.stringify(result)).not.toContain("secret exposed");
|
||||
expect(attempts).toEqual(["failure"]);
|
||||
});
|
||||
it("audits malformed envelopes without copying unvalidated metadata", async () => {
|
||||
const audit = auditRecorder();
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{
|
||||
commandId: "people.client-controlled-target",
|
||||
input: { password: "secret" },
|
||||
reason: "client reason",
|
||||
domain: "people",
|
||||
},
|
||||
dependencies({ audit: audit.writer }),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "VALIDATION" },
|
||||
});
|
||||
expect(audit.entries).toEqual([
|
||||
{
|
||||
userId: 42,
|
||||
action: "housekeeping.command.dispatch",
|
||||
target: "request-envelope",
|
||||
correlationId: result.correlationId,
|
||||
outcome: "denied",
|
||||
ipAddress: "198.51.100.8",
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(audit.entries)).not.toContain("client-controlled");
|
||||
expect(JSON.stringify(audit.entries)).not.toContain("secret");
|
||||
expect(JSON.stringify(audit.entries)).not.toContain("client reason");
|
||||
});
|
||||
|
||||
it("uses canonical command-ID grammar before unknown-command evidence", async () => {
|
||||
const audit = auditRecorder();
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{ commandId: "system.bad\nidentifier", input: {} },
|
||||
dependencies({ audit: audit.writer }),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "VALIDATION" },
|
||||
});
|
||||
expect(audit.entries).toMatchObject([
|
||||
{
|
||||
action: "housekeeping.command.dispatch",
|
||||
target: "request-envelope",
|
||||
outcome: "denied",
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(audit.entries)).not.toContain("bad\\nidentifier");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["null", null],
|
||||
["missing success data", { ok: true, correlationId: "forged" }],
|
||||
[
|
||||
"invalid success flag",
|
||||
{ ok: "yes", data: null, correlationId: "forged" },
|
||||
],
|
||||
[
|
||||
"invalid failure error",
|
||||
{ ok: false, error: null, correlationId: "forged" },
|
||||
],
|
||||
[
|
||||
"throwing getter",
|
||||
Object.defineProperty({}, "ok", {
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
throw new Error("result getter secret exposed");
|
||||
},
|
||||
}),
|
||||
],
|
||||
] as const)(
|
||||
"sanitizes malformed command result: %s",
|
||||
async (label, commandResult) => {
|
||||
const commandId = `system.dispatch.malformed-result-${label.replaceAll(" ", "-")}`;
|
||||
const audit = auditRecorder();
|
||||
register(
|
||||
baseCommand(commandId, {
|
||||
input: z.object({}),
|
||||
execute: async () => commandResult as never,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{ commandId, input: {} },
|
||||
dependencies({ audit: audit.writer }),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "INTERNAL", messageKey: "errors.housekeeping.internal" },
|
||||
});
|
||||
expect(result.correlationId).toMatch(/^[0-9a-f-]{36}$/i);
|
||||
expect(result.correlationId).not.toBe("forged");
|
||||
expect(JSON.stringify(result)).not.toContain("secret exposed");
|
||||
expect(audit.entries.map((entry) => entry.outcome)).toEqual(["failure"]);
|
||||
expect(audit.entries[0]?.correlationId).toBe(result.correlationId);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["empty", ""],
|
||||
["text", "not-an-ip"],
|
||||
["range", "999.1.1.1"],
|
||||
[
|
||||
"pathological",
|
||||
{
|
||||
toString: () => {
|
||||
throw new Error("IP getter secret exposed");
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"rejects invalid server IP without using it in keys or evidence %s",
|
||||
async (label, ipAddress) => {
|
||||
const audit = auditRecorder();
|
||||
const rateKeys: string[] = [];
|
||||
let executed = false;
|
||||
const commandId = `system.dispatch.invalid-ip-${label}`;
|
||||
register(
|
||||
baseCommand(commandId, {
|
||||
input: z.object({}),
|
||||
execute: async (context) => {
|
||||
executed = true;
|
||||
return ok(null, context.correlationId);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{ commandId, input: {} },
|
||||
dependencies({
|
||||
ipAddress: ipAddress as never,
|
||||
audit: audit.writer,
|
||||
rateLimit: async (key) => {
|
||||
rateKeys.push(key);
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "INTERNAL", messageKey: "errors.housekeeping.internal" },
|
||||
});
|
||||
expect(executed).toBe(false);
|
||||
expect(rateKeys).toEqual([]);
|
||||
expect(audit.entries).toMatchObject([
|
||||
{
|
||||
action: "housekeeping.command.dispatch",
|
||||
target: "server-context",
|
||||
outcome: "failure",
|
||||
},
|
||||
]);
|
||||
expect(audit.entries[0]).not.toHaveProperty("ipAddress");
|
||||
expect(JSON.stringify(audit.entries)).not.toContain("not-an-ip");
|
||||
expect(JSON.stringify(audit.entries)).not.toContain("secret exposed");
|
||||
},
|
||||
);
|
||||
it("canonicalizes IPv6 for command context, rate identity, and audit evidence", async () => {
|
||||
const audit = auditRecorder();
|
||||
const rateKeys: string[] = [];
|
||||
let executionIp = "";
|
||||
register(
|
||||
baseCommand("system.dispatch.canonical-ip", {
|
||||
input: z.object({}),
|
||||
execute: async (context) => {
|
||||
executionIp = context.ipAddress;
|
||||
return ok(null, context.correlationId);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await dispatchHousekeepingCommand(
|
||||
{ commandId: "system.dispatch.canonical-ip", input: {} },
|
||||
dependencies({
|
||||
ipAddress: "2001:0DB8:0:0:0:0:0:1",
|
||||
audit: audit.writer,
|
||||
rateLimit: async (key) => {
|
||||
rateKeys.push(key);
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(executionIp).toBe("2001:db8::1");
|
||||
expect(rateKeys).toEqual([
|
||||
"housekeeping-command:42:2001:db8::1:system.dispatch.canonical-ip",
|
||||
]);
|
||||
expect(audit.entries[0]?.ipAddress).toBe("2001:db8::1");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user