From 7b269d1638cad0e736f562f6cf9745c2b77a079b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 9 Apr 2026 12:16:41 -0400 Subject: [PATCH] fix: ssr open graph tags (#27639) fix: SSR open graph tags --- pnpm-lock.yaml | 46 ------------------------- server/package.json | 2 -- server/src/services/api.service.spec.ts | 36 +++++++++++++++++++ server/src/services/api.service.ts | 4 +-- 4 files changed, 38 insertions(+), 50 deletions(-) create mode 100644 server/src/services/api.service.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c26b897d41..9d37c5e09b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -553,9 +553,6 @@ importers: sanitize-filename: specifier: ^1.6.3 version: 1.6.4 - sanitize-html: - specifier: ^2.14.0 - version: 2.17.2 semver: specifier: ^7.6.2 version: 7.7.4 @@ -659,9 +656,6 @@ importers: '@types/react': specifier: ^19.0.0 version: 19.2.14 - '@types/sanitize-html': - specifier: ^2.13.0 - version: 2.16.1 '@types/semver': specifier: ^7.5.8 version: 7.7.1 @@ -5167,9 +5161,6 @@ packages: '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} - '@types/sanitize-html@2.16.1': - resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==} - '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -7971,9 +7962,6 @@ packages: webpack: optional: true - htmlparser2@10.1.0: - resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} - htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} @@ -8303,10 +8291,6 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} - is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -9692,9 +9676,6 @@ packages: parse-numeric-range@1.3.0: resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} - parse-srcset@1.0.2: - resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} - parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -10892,9 +10873,6 @@ packages: sanitize-filename@1.6.4: resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} - sanitize-html@2.17.2: - resolution: {integrity: sha512-EnffJUl46VE9uvZ0XeWzObHLurClLlT12gsOk1cHyP2Ol1P0BnBnsXmShlBmWVJM+dKieQI68R0tsPY5m/B+Jg==} - sass@1.97.1: resolution: {integrity: sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==} engines: {node: '>=14.0.0'} @@ -17459,10 +17437,6 @@ snapshots: '@types/retry@0.12.2': {} - '@types/sanitize-html@2.16.1': - dependencies: - htmlparser2: 10.1.0 - '@types/sax@1.2.7': dependencies: '@types/node': 24.12.2 @@ -20865,13 +20839,6 @@ snapshots: optionalDependencies: webpack: 5.104.1 - htmlparser2@10.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 7.0.1 - htmlparser2@6.1.0: dependencies: domelementtype: 2.3.0 @@ -21200,8 +21167,6 @@ snapshots: dependencies: isobject: 3.0.1 - is-plain-object@5.0.0: {} - is-potential-custom-element-name@1.0.1: optional: true @@ -22933,8 +22898,6 @@ snapshots: parse-numeric-range@1.3.0: {} - parse-srcset@1.0.2: {} - parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -24278,15 +24241,6 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 - sanitize-html@2.17.2: - dependencies: - deepmerge: 4.3.1 - escape-string-regexp: 4.0.0 - htmlparser2: 10.1.0 - is-plain-object: 5.0.0 - parse-srcset: 1.0.2 - postcss: 8.5.8 - sass@1.97.1: dependencies: chokidar: 4.0.3 diff --git a/server/package.json b/server/package.json index 99e58a0e2c..2276b30468 100644 --- a/server/package.json +++ b/server/package.json @@ -110,7 +110,6 @@ "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", - "sanitize-html": "^2.14.0", "semver": "^7.6.2", "sharp": "^0.34.5", "sirv": "^3.0.0", @@ -147,7 +146,6 @@ "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", "@types/react": "^19.0.0", - "@types/sanitize-html": "^2.13.0", "@types/semver": "^7.5.8", "@types/supertest": "^7.0.0", "@types/ua-parser-js": "^0.7.36", diff --git a/server/src/services/api.service.spec.ts b/server/src/services/api.service.spec.ts new file mode 100644 index 0000000000..1152b89981 --- /dev/null +++ b/server/src/services/api.service.spec.ts @@ -0,0 +1,36 @@ +import { ApiService, render } from 'src/services/api.service'; + +describe(ApiService.name, () => { + describe('render', () => { + it('should correctly render open graph tags', () => { + const output = render('', { + title: 'title', + description: 'description', + imageUrl: 'https://demo.immich.app/api/assets/123', + }); + expect(output).toContain(''); + expect(output).toContain(''); + expect(output).toContain(''); + }); + + it('should escape html tags', () => { + expect( + render('', { + title: "Test", + description: 'description', + }), + ).toContain( + '', + ); + }); + + it('should escape quotes', () => { + expect( + render('', { + title: `0;url=https://example.com" http-equiv="refresh`, + description: 'description', + }), + ).toContain(''); + }); + }); +}); diff --git a/server/src/services/api.service.ts b/server/src/services/api.service.ts index 7dd2eb0d4e..c8e6b39b21 100644 --- a/server/src/services/api.service.ts +++ b/server/src/services/api.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotAcceptableException } from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; +import { escape } from 'lodash'; import { readFileSync } from 'node:fs'; -import sanitizeHtml from 'sanitize-html'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { AuthService } from 'src/services/auth.service'; @@ -10,7 +10,7 @@ import { OpenGraphTags } from 'src/utils/misc'; export const render = (index: string, meta: OpenGraphTags) => { const [title, description, imageUrl] = [meta.title, meta.description, meta.imageUrl].map((item) => - item ? sanitizeHtml(item, { allowedTags: [] }) : '', + item ? escape(item) : '', ); const tags = `