mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-27 07:52:03 +00:00
Close four more gaps the widened mutation rule found
Sweeping the smaller targets under the refusal-based selection. All four are cases where two code paths both refuse, so only the reason or the boundary was observable — and nothing observed either. - Approval RESOURCE SCOPING was tested only on its failing side. The check is `record !== "*" && record !== requested`; for a scoped record and a different resource both halves are true, so `&&` and `||` agree and the mutation was invisible. The two cases where they differ are exactly the ones that make scoping useful rather than merely restrictive: a scoped approval must WORK for the resource it names, and a wildcard must work for any resource. Under `||` both stop working entirely. - The request body cap's boundary. A limit of N bytes has to mean N bytes are allowed; `>` accepts exactly N and `>=` rejects it, and the existing test sent 500 bytes at a 64-byte cap, which cannot tell them apart. The cap could have gone off-by-one and surfaced only as requests failing at a size the operator configured as fine. - readEnvelope's existsSync guard. Without it, ENOENT is caught by the same handler as malformed JSON, so a profile that never configured a factor is told its partition envelope is invalid — "restore a backup" instead of "set one up". - sinks.ts and server.ts swept clean under the new rule with no further changes. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
This commit is contained in:
1 parent
9c9544b725
commit
4184de9b9e
2 files changed
+88
-1
No files matched your search
@@ -312,6 +312,30 @@ describe("what the API refuses", () => {
|
||||
body: JSON.stringify({ kind: "password", label: "x".repeat(500) }),
|
||||
});
|
||||
expect(res.status).toBe(413);
|
||||
|
||||
// The BOUNDARY, which nothing checked: a limit of N bytes has to mean N
|
||||
// bytes are allowed. `>` accepts exactly N and `>=` rejects it, and
|
||||
// sending 500 bytes at a 64-byte cap cannot tell the two apart — so the
|
||||
// cap could quietly become off-by-one and only show up as a request
|
||||
// that fails at a size the operator configured as fine.
|
||||
const url = `http://${bound.host}:${bound.port}/api/v1/entry`;
|
||||
const send = (body: string) =>
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${TOKEN}`, "content-type": "application/json" },
|
||||
body,
|
||||
});
|
||||
|
||||
// Exactly 64 bytes of valid JSON: accepted (whatever the route then
|
||||
// makes of it — the point is that it is not refused for its size).
|
||||
const padded = `{"kind":"password","label":"${"x".repeat(64 - 30)}"}`;
|
||||
expect(padded.length).toBe(64);
|
||||
expect((await send(padded)).status).not.toBe(413);
|
||||
|
||||
// One byte more: refused.
|
||||
const overBy1 = `{"kind":"password","label":"${"x".repeat(64 - 29)}"}`;
|
||||
expect(overBy1.length).toBe(65);
|
||||
expect((await send(overBy1)).status).toBe(413);
|
||||
} finally {
|
||||
await small.close();
|
||||
}
|
||||
|
||||
@@ -34,7 +34,13 @@ import {
|
||||
INDEX_FILENAME,
|
||||
type ProgramIo,
|
||||
} from "../src/index.js";
|
||||
import { setFactor, tagForFactor, factorConfigured, partitionPath } from "../src/highRisk.js";
|
||||
import {
|
||||
setFactor,
|
||||
tagForFactor,
|
||||
factorConfigured,
|
||||
partitionPath,
|
||||
envelopePath,
|
||||
} from "../src/highRisk.js";
|
||||
import {
|
||||
issueApproval,
|
||||
listApprovals,
|
||||
@@ -152,6 +158,25 @@ describe("the factor", () => {
|
||||
expect(String((r.error as Error).message)).toContain("SEEDPASS_HIGH_RISK_FACTOR");
|
||||
});
|
||||
|
||||
it("says NOT CONFIGURED rather than blaming the envelope", async () => {
|
||||
// Without the existsSync guard, readFile's ENOENT is caught by the same
|
||||
// handler that catches malformed JSON, so a profile that never set a
|
||||
// factor is told its partition envelope is invalid. Both refuse, so only
|
||||
// the reason is observable — and the reason is the whole difference
|
||||
// between "set one up" and "yours is corrupt, restore a backup".
|
||||
const fresh = await mkdtemp(join(tmpdir(), "seedpass-no-envelope-"));
|
||||
await expect(tagForFactor(fresh, "anything")).rejects.toMatchObject({
|
||||
reason: "high_risk_partition_not_configured",
|
||||
});
|
||||
|
||||
// A file that exists but is not JSON is the case that reason belongs to.
|
||||
await setFactor(fresh, "a-real-factor");
|
||||
await writeFile(envelopePath(fresh), "{not json");
|
||||
await expect(tagForFactor(fresh, "a-real-factor")).rejects.toMatchObject({
|
||||
reason: "invalid_partition_envelope",
|
||||
});
|
||||
});
|
||||
|
||||
it("verifies correctly and rejects a wrong one", async () => {
|
||||
expect(await tagForFactor(appDir, FACTOR)).toMatch(/^[0-9a-f]{64}$/);
|
||||
await expect(tagForFactor(appDir, "wrong")).rejects.toMatchObject({
|
||||
@@ -351,6 +376,44 @@ describe("approval gates", () => {
|
||||
expect((await listApprovals(appDir)).find((a) => a.id === issued.id)!.uses_remaining).toBe(1);
|
||||
});
|
||||
|
||||
it("honours resource scoping in both directions", async () => {
|
||||
// Only the FAILING side of resource scoping was tested, and the check is
|
||||
// `record !== "*" && record !== requested` — for a scoped record and a
|
||||
// different resource both halves are true, so `&&` and `||` agree and a
|
||||
// mutation there was invisible. These are the two cases where they
|
||||
// differ, and they are the cases that make scoping useful rather than
|
||||
// merely restrictive.
|
||||
const scoped = await issueApproval(appDir, {
|
||||
action: "export",
|
||||
resource: "vault-a",
|
||||
ttlSeconds: 300,
|
||||
uses: 1,
|
||||
});
|
||||
// A scoped approval works for the resource it names.
|
||||
expect(
|
||||
await consumeApproval(appDir, {
|
||||
approvalId: scoped.id,
|
||||
action: "export",
|
||||
resource: "vault-a",
|
||||
}),
|
||||
).toEqual({ ok: true, reason: "approval_consumed" });
|
||||
|
||||
// A wildcard approval works for any resource — that is what "*" is for.
|
||||
const wildcard = await issueApproval(appDir, {
|
||||
action: "export",
|
||||
resource: "*",
|
||||
ttlSeconds: 300,
|
||||
uses: 1,
|
||||
});
|
||||
expect(
|
||||
await consumeApproval(appDir, {
|
||||
approvalId: wildcard.id,
|
||||
action: "export",
|
||||
resource: "some-specific-vault",
|
||||
}),
|
||||
).toEqual({ ok: true, reason: "approval_consumed" });
|
||||
});
|
||||
|
||||
it("refuses an expired approval", async () => {
|
||||
const issued = await issueApproval(appDir, {
|
||||
action: "export",
|
||||
|
||||
Reference in new issue
Block a user