fix: cross site scripting issue on /share pages (#16255)

This commit is contained in:
Jason Rasmussen 2025-02-22 06:32:53 -05:00 committed by GitHub
parent caea3a0812
commit e4b6efc1f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 58 additions and 11 deletions

View File

@ -95,7 +95,7 @@ describe('/shared-links', () => {
expect(resp.status).toBe(200); expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html'); expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain( expect(resp.text).toContain(
`<meta name="description" content="${metadataAlbum.assets.length} shared photos & videos" />`, `<meta name="description" content="${metadataAlbum.assets.length} shared photos &amp; videos" />`,
); );
}); });
@ -103,14 +103,14 @@ describe('/shared-links', () => {
const resp = await request(shareUrl).get(`/${linkWithAlbum.key}`); const resp = await request(shareUrl).get(`/${linkWithAlbum.key}`);
expect(resp.status).toBe(200); expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html'); expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(`<meta name="description" content="0 shared photos & videos" />`); expect(resp.text).toContain(`<meta name="description" content="0 shared photos &amp; videos" />`);
}); });
it('should have correct asset count in meta tag for shared asset', async () => { it('should have correct asset count in meta tag for shared asset', async () => {
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`); const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
expect(resp.status).toBe(200); expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html'); expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(`<meta name="description" content="1 shared photos & videos" />`); expect(resp.text).toContain(`<meta name="description" content="1 shared photos &amp; videos" />`);
}); });
it('should have fqdn og:image meta tag for shared asset', async () => { it('should have fqdn og:image meta tag for shared asset', async () => {

View File

@ -59,6 +59,7 @@
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"sanitize-html": "^2.14.0",
"semver": "^7.6.2", "semver": "^7.6.2",
"sharp": "^0.33.0", "sharp": "^0.33.0",
"sirv": "^3.0.0", "sirv": "^3.0.0",
@ -91,6 +92,7 @@
"@types/picomatch": "^3.0.0", "@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5", "@types/pngjs": "^6.0.5",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/sanitize-html": "^2.13.0",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
@ -6042,6 +6044,16 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/sanitize-html": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.13.0.tgz",
"integrity": "sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"htmlparser2": "^8.0.0"
}
},
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.8", "version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
@ -8931,7 +8943,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -10747,6 +10758,15 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-promise": { "node_modules/is-promise": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
@ -12394,6 +12414,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
"license": "MIT"
},
"node_modules/parse5": { "node_modules/parse5": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
@ -14072,6 +14098,20 @@
"truncate-utf8-bytes": "^1.0.0" "truncate-utf8-bytes": "^1.0.0"
} }
}, },
"node_modules/sanitize-html": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.14.0.tgz",
"integrity": "sha512-CafX+IUPxZshXqqRaG9ZClSlfPVjSxI0td7n07hk8QO2oO+9JDnlcL8iM8TWeOXOIBFgIOx6zioTzM53AOMn3g==",
"license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"htmlparser2": "^8.0.0",
"is-plain-object": "^5.0.0",
"parse-srcset": "^1.0.2",
"postcss": "^8.3.11"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.25.0", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",

View File

@ -85,6 +85,7 @@
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"sanitize-html": "^2.14.0",
"semver": "^7.6.2", "semver": "^7.6.2",
"sharp": "^0.33.0", "sharp": "^0.33.0",
"sirv": "^3.0.0", "sirv": "^3.0.0",
@ -117,6 +118,7 @@
"@types/picomatch": "^3.0.0", "@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5", "@types/pngjs": "^6.0.5",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/sanitize-html": "^2.13.0",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Cron, CronExpression, Interval } from '@nestjs/schedule'; import { Cron, CronExpression, Interval } from '@nestjs/schedule';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import sanitizeHtml from 'sanitize-html';
import { ONE_HOUR } from 'src/constants'; import { ONE_HOUR } from 'src/constants';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
@ -12,21 +13,25 @@ import { VersionService } from 'src/services/version.service';
import { OpenGraphTags } from 'src/utils/misc'; import { OpenGraphTags } from 'src/utils/misc';
const render = (index: string, meta: OpenGraphTags) => { const render = (index: string, meta: OpenGraphTags) => {
const [title, description, imageUrl] = [meta.title, meta.description, meta.imageUrl].map((item) =>
item ? sanitizeHtml(item, { allowedTags: [] }) : '',
);
const tags = ` const tags = `
<meta name="description" content="${meta.description}" /> <meta name="description" content="${description}" />
<!-- Facebook Meta Tags --> <!-- Facebook Meta Tags -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="${meta.title}" /> <meta property="og:title" content="${title}" />
<meta property="og:description" content="${meta.description}" /> <meta property="og:description" content="${description}" />
${meta.imageUrl ? `<meta property="og:image" content="${meta.imageUrl}" />` : ''} ${imageUrl ? `<meta property="og:image" content="${imageUrl}" />` : ''}
<!-- Twitter Meta Tags --> <!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${meta.title}" /> <meta name="twitter:title" content="${title}" />
<meta name="twitter:description" content="${meta.description}" /> <meta name="twitter:description" content="${description}" />
${meta.imageUrl ? `<meta name="twitter:image" content="${meta.imageUrl}" />` : ''}`; ${imageUrl ? `<meta name="twitter:image" content="${imageUrl}" />` : ''}`;
return index.replace('<!-- metadata:tags -->', tags); return index.replace('<!-- metadata:tags -->', tags);
}; };