fix(domain): reject malformed base64 before byte-range checks
This commit is contained in:
1 parent
21f9af9d93
commit
31dc035041
7 files changed
+76
-90
No files matched your search
@@ -178,59 +178,65 @@ it("cancels before invocation and after an acquired observation", async () => {
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
|
||||
});
|
||||
|
||||
it.each(["different-path", "invalid-digest", "outside-range"])(
|
||||
"rejects provider evidence at the next boundary: %s",
|
||||
async (problem) => {
|
||||
const fixture = recordedCrashFixture();
|
||||
if (problem === "different-path") fixture.artifact.path = "/different.elf";
|
||||
if (problem === "invalid-digest") fixture.artifact.sha256 = "unobserved";
|
||||
if (problem === "outside-range")
|
||||
fixture.segments.push({
|
||||
index: 0,
|
||||
type: "PT_LOAD",
|
||||
offset: "0x200",
|
||||
header_location: { offset: "0x0", bytes: "0x38" },
|
||||
file_backing: "file",
|
||||
file_size: "0x1",
|
||||
memory_size: "0x1",
|
||||
virtual_address: "0x20000000000001",
|
||||
physical_address: "0x0",
|
||||
alignment: "0x1",
|
||||
flags: "0x5",
|
||||
});
|
||||
const service = new RecordedCrashService({
|
||||
inspectAndClose(input, options) {
|
||||
return this.inspect(input, options);
|
||||
},
|
||||
identity: RECORDED_CRASH_TEST_PROVIDER,
|
||||
inspect: () => Promise.resolve(ok(fixture)),
|
||||
it.each([
|
||||
"different-path",
|
||||
"invalid-digest",
|
||||
"outside-range",
|
||||
"owner-base64",
|
||||
"descriptor-base64",
|
||||
])("rejects provider evidence at the next boundary: %s", async (problem) => {
|
||||
const fixture = recordedCrashFixture();
|
||||
if (problem === "different-path") fixture.artifact.path = "/different.elf";
|
||||
if (problem === "invalid-digest") fixture.artifact.sha256 = "unobserved";
|
||||
if (problem === "owner-base64") fixture.notes[0]!.owner_bytes_base64 = "A";
|
||||
if (problem === "descriptor-base64")
|
||||
fixture.notes[0]!.descriptor_bytes_base64 = "A";
|
||||
if (problem === "outside-range")
|
||||
fixture.segments.push({
|
||||
index: 0,
|
||||
type: "PT_LOAD",
|
||||
offset: "0x200",
|
||||
header_location: { offset: "0x0", bytes: "0x38" },
|
||||
file_backing: "file",
|
||||
file_size: "0x1",
|
||||
memory_size: "0x1",
|
||||
virtual_address: "0x20000000000001",
|
||||
physical_address: "0x0",
|
||||
alignment: "0x1",
|
||||
flags: "0x5",
|
||||
});
|
||||
expect(
|
||||
await service.inspect({ path: "/artifacts/source-owned.core" }),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisOutputError",
|
||||
capturedOutput: fixture.diagnostics,
|
||||
},
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const lateInvalid = await new RecordedCrashService({
|
||||
inspectAndClose(input, options) {
|
||||
return this.inspect(input, options);
|
||||
},
|
||||
identity: RECORDED_CRASH_TEST_PROVIDER,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
return Promise.resolve(ok(fixture));
|
||||
},
|
||||
}).inspect(
|
||||
{ path: "/artifacts/source-owned.core" },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(lateInvalid).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisOutputError" },
|
||||
});
|
||||
},
|
||||
);
|
||||
const service = new RecordedCrashService({
|
||||
inspectAndClose(input, options) {
|
||||
return this.inspect(input, options);
|
||||
},
|
||||
identity: RECORDED_CRASH_TEST_PROVIDER,
|
||||
inspect: () => Promise.resolve(ok(fixture)),
|
||||
});
|
||||
expect(
|
||||
await service.inspect({ path: "/artifacts/source-owned.core" }),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisOutputError",
|
||||
capturedOutput: fixture.diagnostics,
|
||||
},
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const lateInvalid = await new RecordedCrashService({
|
||||
inspectAndClose(input, options) {
|
||||
return this.inspect(input, options);
|
||||
},
|
||||
identity: RECORDED_CRASH_TEST_PROVIDER,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
return Promise.resolve(ok(fixture));
|
||||
},
|
||||
}).inspect(
|
||||
{ path: "/artifacts/source-owned.core" },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(lateInvalid).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisOutputError" },
|
||||
});
|
||||
});
|
||||
@@ -158,12 +158,7 @@ export const goBinarySchema = z
|
||||
["module_bytes_base64", info.module_location, "module"],
|
||||
] as const) {
|
||||
const encoded = info[field];
|
||||
const padding = encoded.endsWith("==")
|
||||
? 2
|
||||
: encoded.endsWith("=")
|
||||
? 1
|
||||
: 0;
|
||||
if ((encoded.length / 4) * 3 - padding !== range.bytes)
|
||||
if (Buffer.byteLength(encoded, "base64") !== range.bytes)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["build_info", field],
|
||||
|
||||
@@ -426,17 +426,11 @@ const validatePackedRelativeTables = (
|
||||
tableIndex,
|
||||
"location",
|
||||
]);
|
||||
const encodedBytes =
|
||||
(table.encoded_bytes_base64.length * 3) / 4 -
|
||||
(table.encoded_bytes_base64.endsWith("==")
|
||||
? 2
|
||||
: table.encoded_bytes_base64.endsWith("=")
|
||||
? 1
|
||||
: 0);
|
||||
if (
|
||||
!Number.isInteger(encodedBytes) ||
|
||||
BigInt(encodedBytes) !== BigInt(table.location.bytes)
|
||||
)
|
||||
const encodedBytes = Buffer.byteLength(
|
||||
table.encoded_bytes_base64,
|
||||
"base64",
|
||||
);
|
||||
if (BigInt(encodedBytes) !== BigInt(table.location.bytes))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["packed_relative_relocations", tableIndex],
|
||||
|
||||
@@ -317,15 +317,10 @@ export const recordedCrashSchema = recordedCrashObjectSchema.superRefine(
|
||||
fail("Invalid original note identity or segment reference.");
|
||||
continue;
|
||||
}
|
||||
const decodedLength = (raw: string) =>
|
||||
BigInt(
|
||||
(raw.length / 4) * 3 -
|
||||
(raw.endsWith("==") ? 2 : raw.endsWith("=") ? 1 : 0),
|
||||
);
|
||||
if (
|
||||
decodedLength(note.owner_bytes_base64) !==
|
||||
BigInt(Buffer.byteLength(note.owner_bytes_base64, "base64")) !==
|
||||
BigInt(note.owner_location.bytes) ||
|
||||
decodedLength(note.descriptor_bytes_base64) !==
|
||||
BigInt(Buffer.byteLength(note.descriptor_bytes_base64, "base64")) !==
|
||||
BigInt(note.descriptor_location.bytes)
|
||||
)
|
||||
fail("Raw note lengths differ from their source ranges.");
|
||||
|
||||
@@ -32,13 +32,7 @@ const paddingMatches = (
|
||||
segment: Segment | undefined,
|
||||
): boolean => {
|
||||
const bytes = BigInt(item.location.bytes);
|
||||
const decodedLength =
|
||||
(item.bytes_base64.length / 4) * 3 -
|
||||
(item.bytes_base64.endsWith("==")
|
||||
? 2
|
||||
: item.bytes_base64.endsWith("=")
|
||||
? 1
|
||||
: 0);
|
||||
const decodedLength = Buffer.byteLength(item.bytes_base64, "base64");
|
||||
return (
|
||||
bytes > 0n &&
|
||||
bytes < 12n &&
|
||||
|
||||
@@ -3,8 +3,7 @@ import { isPaddedBase64 } from "./stringPatterns.js";
|
||||
/** Validate padded syntax without imposing canonical zero pad bits. */
|
||||
export const paddedBase64ByteLength = (value: string): number | undefined => {
|
||||
if (!isPaddedBase64(value)) return undefined;
|
||||
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
||||
return (value.length / 4) * 3 - padding;
|
||||
return Buffer.byteLength(value, "base64");
|
||||
};
|
||||
|
||||
export const decodePaddedBase64 = (value: string): Buffer | undefined =>
|
||||
|
||||
@@ -32,10 +32,13 @@ const paddedBase64SchemaRules = [
|
||||
{ not: { pattern: INVALID_BASE64_PADDING_PATTERN.source } },
|
||||
];
|
||||
|
||||
/** Canonical padded base64 with zero pad bits, including the empty encoding. */
|
||||
/** Canonical padded base64 with zero pad bits; invalid bytes never enter enclosing range checks. */
|
||||
export const canonicalBase64Schema = z
|
||||
.string()
|
||||
.refine(isCanonicalBase64, "Expected canonical padded base64")
|
||||
.refine(isCanonicalBase64, {
|
||||
message: "Expected canonical padded base64",
|
||||
abort: true,
|
||||
})
|
||||
.meta({
|
||||
allOf: [
|
||||
...paddedBase64SchemaRules,
|
||||
|
||||
Reference in new issue
Block a user