From 67152772d24d26ebbdb4aba5c1fc4b16ab0677fa Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Tue, 20 Oct 2026 15:25:15 +0200 Subject: [PATCH] fix(server): anchor plugin allowedHosts matching (#32560) (cherry picked from commit 4acf935ba3ee7b6171e3965cb3ce47b2ba9bad1a) --- .../services/workflow-execution.service.ts | 25 ++++++++--------- server/src/utils/workflow.spec.ts | 27 ++++++++++++++++++- server/src/utils/workflow.ts | 12 +++++++++ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/server/src/services/workflow-execution.service.ts b/server/src/services/workflow-execution.service.ts index a3a729494..dbfa38359 100644 --- a/server/src/services/workflow-execution.service.ts +++ b/server/src/services/workflow-execution.service.ts @@ -31,6 +31,7 @@ import { AlbumService } from 'src/services/album.service.js'; import { AssetService } from 'src/services/asset.service.js'; import { BaseService } from 'src/services/base.service.js'; import { TagService } from 'src/services/tag.service.js'; +import { isAllowedHost } from 'src/utils/workflow.js'; const dummy = () => { throw new Error( @@ -93,22 +94,18 @@ export class WorkflowExecutionService extends BaseService { ] >(async (authDto, context, args) => { const hostname = new URL(args[0]).hostname; - - for (const pattern of context.allowedHosts) { - const regex = new RegExp(pattern.replaceAll('.', String.raw`\.`).replaceAll('*', '.*')); - if (regex.test(hostname)) { - // eslint-disable-next-line unicorn/no-invalid-argument-count - const res = await fetch(...args); - - return { - ok: res.ok, - status: res.status, - body: await res.text(), - }; - } + if (!isAllowedHost(hostname, context.allowedHosts)) { + throw new Error('Hostname did not match any listed in methods[].allowedHosts in the plugin manifest'); } - throw new Error('Hostname did not match any listed in methods[].allowedHosts in the plugin manifest'); + // eslint-disable-next-line unicorn/no-invalid-argument-count + const res = await fetch(...args); + + return { + ok: res.ok, + status: res.status, + body: await res.text(), + }; }); const bulkTagAssets = this.wrap<[dto: TagBulkAssetsDto]>((authDto, ctx, args) => tagService.bulkTagAssets(authDto, ...args), diff --git a/server/src/utils/workflow.spec.ts b/server/src/utils/workflow.spec.ts index 80bc468d9..5bcb0629e 100644 --- a/server/src/utils/workflow.spec.ts +++ b/server/src/utils/workflow.spec.ts @@ -1,6 +1,6 @@ import { WorkflowTrigger } from '@immich/plugin-sdk'; import { WorkflowType } from 'src/enum.js'; -import { isMethodCompatible } from 'src/utils/workflow.js'; +import { isAllowedHost, isMethodCompatible } from 'src/utils/workflow.js'; const tests: Array<{ trigger: WorkflowTrigger; types: WorkflowType[]; expected: boolean }> = [ { @@ -35,3 +35,28 @@ describe(isMethodCompatible.name, () => { expect(isMethodCompatible({ types }, trigger)).toBe(expected); }); }); + +describe(isAllowedHost.name, () => { + it('should match an exact host', () => { + expect(isAllowedHost('api.example.com', ['api.example.com'])).toBe(true); + expect(isAllowedHost('API.Example.com', ['api.example.com'])).toBe(true); + }); + + it('should not match a host that merely contains the pattern', () => { + expect(isAllowedHost('api.example.com.attacker.net', ['api.example.com'])).toBe(false); + expect(isAllowedHost('notapi.example.com', ['api.example.com'])).toBe(false); + expect(isAllowedHost('apixexample.com', ['api.example.com'])).toBe(false); + }); + + it('should support wildcards', () => { + expect(isAllowedHost('a.example.com', ['*.example.com'])).toBe(true); + expect(isAllowedHost('a.b.example.com', ['*.example.com'])).toBe(true); + expect(isAllowedHost('example.com', ['*.example.com'])).toBe(false); + expect(isAllowedHost('example.com.attacker.net', ['*.example.com'])).toBe(false); + expect(isAllowedHost('anything.at.all', ['*'])).toBe(true); + }); + + it('should reject everything when no hosts are listed', () => { + expect(isAllowedHost('example.com', [])).toBe(false); + }); +}); diff --git a/server/src/utils/workflow.ts b/server/src/utils/workflow.ts index cdce0c30a..c763eec2d 100644 --- a/server/src/utils/workflow.ts +++ b/server/src/utils/workflow.ts @@ -69,3 +69,15 @@ export const parseMethodString = (method: string) => { const methodName = matches.groups?.method; return { pluginName, version, methodName }; }; + +export const isAllowedHost = (hostname: string, allowedHosts: string[]) => { + const target = hostname.toLowerCase(); + return allowedHosts.some((pattern) => { + const source = pattern + .toLowerCase() + .split('*') + .map((part) => part.replaceAll(/[$()+.?[\\\]^{|}]/g, String.raw`\$&`)) + .join('.*'); + return new RegExp(`^${source}$`).test(target); + }); +};