diff --git a/server/api/registry/badge/[type]/[...pkg].get.ts b/server/api/registry/badge/[type]/[...pkg].get.ts index fdc994233c..cbab8ac374 100644 --- a/server/api/registry/badge/[type]/[...pkg].get.ts +++ b/server/api/registry/badge/[type]/[...pkg].get.ts @@ -87,6 +87,14 @@ function measureDefaultTextWidth(text: string): number { return Math.max(MIN_BADGE_TEXT_WIDTH, Math.round(text.length * CHAR_WIDTH) + BADGE_PADDING_X * 2) } +function escapeXML(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + function measureShieldsTextLength(text: string): number { const measuredWidth = measureTextWidth(text, SHIELDS_FONT_SHORTHAND) @@ -108,9 +116,11 @@ function renderDefaultBadgeSvg(params: { const rightWidth = measureDefaultTextWidth(finalValue) const totalWidth = leftWidth + rightWidth const height = 20 + const escapedLabel = escapeXML(finalLabel) + const escapedValue = escapeXML(finalValue) return ` - + @@ -119,8 +129,8 @@ function renderDefaultBadgeSvg(params: { - ${finalLabel} - ${finalValue} + ${escapedLabel} + ${escapedValue} `.trim() @@ -141,7 +151,9 @@ function renderShieldsBadgeSvg(params: { const rightWidth = rightTextLength + SHIELDS_LABEL_PADDING_X * 2 const totalWidth = leftWidth + rightWidth const height = 20 - const title = `${finalLabel}: ${finalValue}` + const escapedLabel = escapeXML(finalLabel) + const escapedValue = escapeXML(finalValue) + const title = `${escapedLabel}: ${escapedValue}` const leftCenter = Math.round((leftWidth / 2) * 10) const rightCenter = Math.round((leftWidth + rightWidth / 2) * 10) @@ -163,10 +175,10 @@ function renderShieldsBadgeSvg(params: { - - ${finalLabel} - - ${finalValue} + + ${escapedLabel} + + ${escapedValue} `.trim() diff --git a/shared/schemas/social.ts b/shared/schemas/social.ts index f0c018e11d..1e3305941b 100644 --- a/shared/schemas/social.ts +++ b/shared/schemas/social.ts @@ -13,7 +13,19 @@ export type PackageLikeBody = v.InferOutput // TODO: add 'avatar' export const ProfileEditBodySchema = v.object({ displayName: v.pipe(v.string(), v.maxLength(640)), - website: v.optional(v.union([v.literal(''), v.pipe(v.string(), v.url())])), + website: v.optional( + v.union([ + v.literal(''), + v.pipe( + v.string(), + v.url(), + v.check( + url => url.startsWith('https://') || url.startsWith('http://'), + 'Website must use http or https', + ), + ), + ]), + ), description: v.optional(v.pipe(v.string(), v.maxLength(2560))), })