fix(server): anchor plugin allowedHosts matching (#32560)
(cherry picked from commit 4acf935ba3ee7b6171e3965cb3ce47b2ba9bad1a)
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user