fix(pe): enforce certificate name string alphabets (#8494)

* fix(pe): enforce certificate name string alphabets

* test(mcp): assert structured absolute-path diagnostics

* test(mcp): assert typed selector and stdio diagnostics

* fix(pe): bound IA5 name scans before validation
This commit is contained in:
ShuaixinHuang authored and GitHub committed 2026-11-01 21:24:01 +08:00
1 parent 32c253ba31
commit aa48d494e9
3 files changed
+137 -2

No files matched your search

+12 -2
View File
@@ -186,9 +186,19 @@ const decodeDirectoryString = (
return decodeUtf32Be(node.content, budget);
case 0x1e:
return decodeBmpString(node.content, budget);
case 0x0c:
case 0x13:
case 0x13: {
const decoded = decodeUtf8(node.content, budget);
if (/[^A-Za-z0-9 '()+,\-./:=?]/u.test(decoded))
return fail("PrintableString holds a character outside its alphabet.");
return decoded;
}
case 0x16:
chargeDecodedName(budget, node.content.length);
if (node.content.some((byte) => byte > 0x7f))
return fail("IA5String holds a non-ASCII character.");
// Validated ASCII needs no UTF-8 round trip or second budget charge.
return node.content.toString("ascii");
case 0x0c:
case 0x17:
case 0x18:
return decodeUtf8(node.content, budget);
+82
View File
@@ -829,3 +829,85 @@ it("preserves valid BMPString code units on both sides of the surrogate range",
parseCertificate(readAsn1(namedCertificate(name, name))).subject.text,
).toBe("commonName=\ud7ff\ue000\ufeff");
});
it.each([
[
"PrintableString",
0x13,
[
"name@host",
"under_score",
"A\u0000B",
"A\tB",
"A\n",
"A\r",
"A\u007f",
"é",
"中",
],
],
["IA5String", 0x16, ["é", "中"]],
] as const)(
"rejects characters outside the %s alphabet in certificate names",
(_label, tag, values) => {
for (const value of values) {
const invalid = directoryName(der(tag, Buffer.from(value, "utf8")));
const valid = directoryName(der(0x0c, Buffer.from(value, "utf8")));
for (const [issuer, subject] of [
[invalid, valid],
[valid, invalid],
] as const) {
expect(() =>
parseCertificate(readAsn1(namedCertificate(issuer, subject))),
).toThrow(PeCertificateAsn1Failure);
}
const certificate = parseCertificate(
readAsn1(namedCertificate(valid, valid)),
);
expect(certificate.issuer.attributes[0]?.value).toBe(value);
expect(certificate.subject.attributes[0]?.value).toBe(value);
}
},
);
it("bounds IA5String inspection before alphabet validation and charges text once", () => {
const ascii = Buffer.alloc(Math.floor(MAX_DECODED_NAME_BYTES / 3), 0x41);
const within = directoryName(der(0x16, ascii));
const certificate = parseCertificate(
readAsn1(namedCertificate(within, within)),
);
expect(certificate.issuer.attributes[0]?.value).toBe(ascii.toString("ascii"));
expect(certificate.subject.attributes[0]?.value).toBe(
ascii.toString("ascii"),
);
for (const ending of [Buffer.from("A"), Buffer.from("é")]) {
const oversized = Buffer.concat([
Buffer.alloc(MAX_DECODED_NAME_BYTES, 0x41),
ending,
]);
const parseOversized = () =>
parseCertificate(
readAsn1(namedCertificate(directoryName(der(0x16, oversized)), within)),
);
expect(parseOversized).toThrow(
expect.objectContaining({
exhausted: { allowance: "decoded-text", limit: MAX_DECODED_NAME_BYTES },
}),
);
}
});
it.each([
[0x13, "AZaz09 '()+,-./:=?"],
[0x16, "mail_box@example.test\u0000\t\u007f"],
] as const)(
"preserves valid character boundaries for name tag %i",
(tag, value) => {
const name = directoryName(der(tag, Buffer.from(value, "ascii")));
const certificate = parseCertificate(
readAsn1(namedCertificate(name, name)),
);
expect(certificate.issuer.attributes[0]?.value).toBe(value);
expect(certificate.subject.attributes[0]?.value).toBe(value);
},
);
@@ -346,3 +346,46 @@ cliTest.for([
);
},
);
cliTest.for([
[0x13, "REA@Fixture"],
[0x16, "REAéFixture"],
] as const)(
"rejects an invalid certificate name alphabet for tag %i through the public CLI",
async ([tag, name], { cli }) => {
const root = await createTestTempDirectory("rea-pe-cert-name-alphabet-");
const path = join(root, "name.exe");
const payload = pkcs7SignedDataFixture(name);
const content = Buffer.from(name, "utf8");
const marker = Buffer.concat([
Buffer.from([0x0c, content.length]),
content,
]);
const at = payload.indexOf(marker);
expect(at).toBeGreaterThanOrEqual(0);
payload[at] = tag;
await writeFile(path, peCertificateFixture(false, payload));
const invalid = await cli.run({
arguments: ["inspect-pe-certificates", path, "--json"],
});
expect(invalid.exitCode).toBe(1);
expect(invalid.json).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_pe_certificates" },
});
// Only restore the string tag: the same bytes are valid UTF8String content.
payload[at] = 0x0c;
await writeFile(path, peCertificateFixture(false, payload));
const valid = await cli.run({
arguments: ["inspect-pe-certificates", path, "--json"],
});
expect(valid.exitCode, valid.stderr).toBe(0);
const evidence = toolContract("inspect_pe_certificates").outputSchema.parse(
valid.json,
);
expect(
evidence.normalized_result.entries[0]?.pkcs7?.certificates[0]?.issuer
.attributes[0]?.value,
).toBe(name);
},
);