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 = `