Skip to content

Commit 5862f95

Browse files
committed
Validate package names for mergebot comment
1 parent f5ee98e commit 5862f95

2 files changed

Lines changed: 28 additions & 4 deletions

File tree

packages/mergebot/src/compute-pr-actions.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,12 @@ function createWelcomeComment(info: ExtendedPrInfo, post: (c: Comments.Comment)
513513

514514
const announceList = (what: string, xs: readonly string[]) => `${xs.length} ${what}${xs.length !== 1 ? "s" : ""}`;
515515
const usersToString = (users: string[]) => users.map((u) => (info.isAuthor(u) ? "✎" : "") + "@" + u).join(", ");
516+
// Strip characters that would escape an inline-code span or inject Markdown structure
517+
// (links, headers, lists, etc.) when an attacker-controlled value is interpolated into the
518+
// welcome comment.
519+
const mdSafe = (s: string) => s.replace(/[`\[\]()\r\n]/g, "");
516520
const reviewLink = (f: FileInfo) =>
517-
`[\`${f.path.replace(/^types\/(.*\/)/, "$1")}\`](${urls.review(
521+
`[\`${mdSafe(f.path.replace(/^types\/(.*\/)/, "$1"))}\`](${urls.review(
518522
info.pr_number,
519523
)}/${info.headCommitOid}#diff-${sha256(f.path)})`;
520524

@@ -535,7 +539,9 @@ function createWelcomeComment(info: ExtendedPrInfo, post: (c: Comments.Comment)
535539
for (const p of info.pkgInfo) {
536540
if (p.name === null) continue;
537541
const kind = p.kind === "add" ? " (*new!*)" : p.kind === "delete" ? " (*probably deleted!*)" : "";
538-
const urlPart = p.name.replace(/^(.*?)__(.)/, "@$1/$2");
542+
// p.name is validated to /^[a-z0-9][a-z0-9._-]*$/ in categorizeFile, so it is safe to
543+
// interpolate directly into Markdown and URLs without further escaping.
544+
const urlPart = encodeURI(p.name.replace(/^(.*?)__(.)/, "@$1/$2"));
539545
const authorIsOwner = !p.owners.some(info.isAuthor) ? [] : [`(author is owner)`];
540546
display(
541547
[

packages/mergebot/src/pr-info.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,9 @@ async function categorizeFile(
445445
): Promise<[string | null, FileInfo]> {
446446
const pkg = /^types\/(.*?)\/.*$/.exec(path)?.[1];
447447
if (!pkg) return [null, { path, kind: "infrastructure" }];
448+
// Treat unrecognized directory names as infrastructure rather than trusting the string in
449+
// downstream comment/label/URL construction.
450+
if (!isValidPackageDirectoryName(pkg)) return [null, { path, kind: "infrastructure" }];
448451

449452
if (isDeclarationPath(path)) return [pkg, { path, kind: "definition" }];
450453
if (/\.(?:[cm]?ts|tsx)$/.test(path)) return [pkg, { path, kind: "test" }];
@@ -754,8 +757,23 @@ export async function getOwnersOfPackage(
754757
} catch (e) {
755758
if (e instanceof Error) return new Error(`error parsing owners: ${e.message}`);
756759
}
757-
return noNullish(parsed!.contributors.map((c) => c.githubUsername));
760+
return noNullish(parsed!.contributors.map((c) => c.githubUsername)).filter(isValidGithubUsername);
758761
}
759762

760-
return noNullish(packageJsonObj.owners?.map((c: any) => c?.githubUsername));
763+
return noNullish(packageJsonObj.owners?.map((c: any) => c?.githubUsername)).filter(isValidGithubUsername);
764+
}
765+
766+
// GitHub usernames: alphanumeric or single hyphens (plus underscores for Enterprise Managed
767+
// Users). Validating here prevents an untrusted PR-head package.json from injecting Markdown
768+
// (links, headers, newlines, etc.) into bot comment bodies under @typescript-bot's identity.
769+
function isValidGithubUsername(name: unknown): name is string {
770+
return typeof name === "string" && /^[A-Za-z0-9_-]+$/.test(name);
771+
}
772+
773+
// DefinitelyTyped package directory names: lowercase letters, digits, dots, hyphens, and the
774+
// `__` scope-mangle separator (e.g. `react-native`, `gapi.client.youtube-v3`, `google__maps`).
775+
// Validating here prevents an attacker-crafted path like `types/foo](evil)/index.d.ts` from
776+
// injecting Markdown when the directory name is interpolated into bot comment bodies.
777+
function isValidPackageDirectoryName(name: string): boolean {
778+
return /^[a-z0-9][a-z0-9._-]*$/.test(name);
761779
}

0 commit comments

Comments
 (0)