Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 101 additions & 100 deletions dist/shared/index.js

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions src/publication-safety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,24 @@ test("named credentials are checked recursively while exact synthetic literals r
}),
).toThrow("personal-path");
});

test("nested JSON and rendered metadata cannot hide private text", () => {
const credential = "SYNTHETIC".repeat(8);
const path = "/" + "Users/fictional/session";
for (const value of [
{ body: JSON.stringify({ API_KEY: credential }) },
'"api_key": "' + credential + '"',
{ body: JSON.stringify({ CLOUDFLARE_API_KEY: credential }) },
{ encoding: "base64", content: Buffer.from(path, "utf16le").toString("base64") },
["&#47", ";Users&#47", ";fictional&#47", ";session"].join(""),
"notes/" + "fictional-session/report.json",
])
expect(() => assertPublicMessage(value)).toThrow("Public message refused:");
for (const value of [
"ROOTFORM_DATADOG_CLOUDFLARE_KEY_SENTINEL",
"ROOTFORM_DATADOG_FASTLY_KEY_SENTINEL",
"ROOTFORM_HCP_DATADOG_API_SENTINEL",
"ROOTFORM_ATLAS_OBSERVABILITY_SECRET",
])
expect(() => assertPublicMessage('api_key = "' + value + '"')).not.toThrow();
});
87 changes: 65 additions & 22 deletions src/publication-safety.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ export type PublicationIssue = { rule: string; line: number };

// Exact non-credential literals used by public synthetic provider fixtures.
const syntheticCredentialValues = new Set([
"fixture000000000000000000000000000000000",
"ROOTFORM_DATADOG_PROVIDER_API_KEY_SENTINEL",
"ROOTFORM_NEWRELIC_BOUNDARY_SECRET_SENTINEL",
"ROOTFORM_NEWRELIC_PROVIDER_KEY_SENTINEL",
"ROOTFORM_OKTA_BOUNDARY_SECRET_SENTINEL",
"ROOTFORM_OKTA_PROVIDER_TOKEN_SENTINEL",
"ROOTFORM_DATADOG_CLOUDFLARE_KEY_SENTINEL",
"ROOTFORM_DATADOG_FASTLY_KEY_SENTINEL",
"ROOTFORM_HCP_DATADOG_API_SENTINEL",
Expand All @@ -15,7 +21,7 @@ const rules: Array<[string, RegExp]> = [
],
[
"credential",
/BEGIN (?:RSA|OPENSSH|EC|DSA)? ?PRIVATE KEY|(?:github_pat_|gh[pousr]_|npm_)[A-Za-z0-9_]{12,}|\b(?:AKIA|ASIA)[A-Z0-9]{16}\b|xox[abprs]-[A-Za-z0-9-]{12,}|AIza[A-Za-z0-9_-]{35}|\bBearer\s+[A-Za-z0-9._~+/-]{24,}=*|\b(?:CLOUDFLARE_API_TOKEN|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|API_TOKEN|API_KEY)\s*[:=]\s*["\x27]?[A-Za-z0-9_+./~-]{24,}/gu,
/BEGIN (?:RSA|OPENSSH|EC|DSA)? ?PRIVATE KEY|(?:github_pat_|gh[pousr]_|npm_)[A-Za-z0-9_]{12,}|\b(?:AKIA|ASIA)[A-Z0-9]{16}\b|xox[abprs]-[A-Za-z0-9-]{12,}|AIza[A-Za-z0-9_-]{35}|\bBearer\s+[A-Za-z0-9._~+/-]{24,}=*/gu,
],
["private-workspace", /\b(?:rootform-dev\/)?notes\/[A-Za-z0-9][A-Za-z0-9._/-]*/gu],
[
Expand All @@ -33,27 +39,66 @@ export function publicationIssues(text: string): PublicationIssue[] {
const found: PublicationIssue[] = [];
const scan = (source: string) => {
for (let depth = 0; depth < 3; depth++) {
const decoded = source.replace(/(?:%[0-9a-f]{2})+/giu, (encoded) => {
try {
return decodeURIComponent(encoded);
} catch {
return encoded;
}
});
const decoded = source
.replace(/(?:%[0-9a-f]{2})+/giu, (encoded) => {
try {
return decodeURIComponent(encoded);
} catch {
return encoded;
}
})
.replace(
/&(?:#(x[0-9a-f]+|[0-9]+)|amp|sol|colon|quot|apos);/giu,
(entity, numeric: string | undefined) => {
if (numeric) {
const code = numeric.toLowerCase().startsWith("x")
? Number.parseInt(numeric.slice(1), 16)
: Number(numeric);
return code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : entity;
}
return (
(
{
"&amp;": "&",
"&sol;": "/",
"&colon;": ":",
"&quot;": '"',
"&apos;": "'",
} as Record<string, string>
)[entity.toLowerCase()] ?? entity
);
},
)
.replaceAll("\0", "");
if (decoded === source) break;
source = decoded;
}
for (const [rule, pattern] of rules)
for (const match of source.matchAll(pattern))
found.push({ rule, line: source.slice(0, match.index).split("\n").length });
const namedCredential =
/\b(?:CLOUDFLARE_API_(?:TOKEN|KEY)|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|API_TOKEN|API_KEY)["']?\s*[:=]\s*["']?([A-Za-z0-9_+./~=-]{24,})/giu;
for (const match of source.matchAll(namedCredential))
if (!syntheticCredentialValues.has(match[1] ?? ""))
found.push({ rule: "credential", line: source.slice(0, match.index).split("\n").length });
};
const inspect = (value: unknown, depth: number) => {
if (depth > 128) {
found.push({ rule: "uninspectable-content", line: 1 });
return;
}
if (typeof value === "string") scan(value);
else if (Array.isArray(value)) for (const item of value) inspect(item, depth + 1);
if (typeof value === "string") {
scan(value);
if (/^\s*[[{]/u.test(value)) {
let nested: unknown;
try {
nested = JSON.parse(value);
} catch {
return;
}
inspect(nested, depth + 1);
}
} else if (Array.isArray(value)) for (const item of value) inspect(item, depth + 1);
else if (value !== null && typeof value === "object") {
const payload = value as Record<string, unknown>;
if (
Expand All @@ -70,21 +115,14 @@ export function publicationIssues(text: string): PublicationIssue[] {
const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(
Uint8Array.from(atob(payload.content), (character) => character.charCodeAt(0)),
);
scan(decoded);
let structured: unknown;
try {
structured = JSON.parse(decoded);
} catch {
structured = undefined;
}
if (structured !== undefined) inspect(structured, depth + 1);
inspect(decoded, depth + 1);
} catch {
found.push({ rule: "uninspectable-content", line: 1 });
}
}
for (const [key, item] of Object.entries(payload)) {
if (
/^(?:CLOUDFLARE_API_TOKEN|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|API_TOKEN|API_KEY)$/iu.test(
/^(?:CLOUDFLARE_API_(?:TOKEN|KEY)|AWS_SECRET_ACCESS_KEY|OPENAI_API_KEY|API_TOKEN|API_KEY)$/iu.test(
key,
) &&
typeof item === "string" &&
Expand Down Expand Up @@ -125,14 +163,19 @@ export function assertPublicMessage(value: unknown): void {
if (issue) throw new Error(`Public message refused: ${issue.rule}`);
}

/** Consumer reports may contain user-authored model data and local paths.
* Credential-shaped values are still refused before any public write. */
export function assertReportMessage(
value: unknown,
repository = process.env.GITHUB_REPOSITORY ?? "",
): void {
if (
["rootform-dev/rootform", "rootform-dev/action", "rootform-dev/editors"].includes(repository)
[
"rootform-dev/rootform",
"rootform-dev/action",
"rootform-dev/editors",
"rootform-dev/extensions",
"rootform-dev/action-qualification",
"rootform-dev/homebrew-tap",
].includes(repository)
) {
assertPublicMessage(value);
return;
Expand Down
Loading