fix: check plugin http.post url against origin and add regression tests
This commit is contained in:
@@ -148,3 +148,46 @@ describe('toast bridge', () => {
|
||||
expect(api.toast.warning).toBeInstanceOf(Function);
|
||||
});
|
||||
});
|
||||
|
||||
describe('http.post path validation', () => {
|
||||
function makeApi(permissions: string[] = ['http:post']) {
|
||||
return createPluginAPI(makePlugin({ permissions }));
|
||||
}
|
||||
|
||||
it('rejects protocol-relative URLs like //evil.example', async () => {
|
||||
const api = makeApi();
|
||||
await expect(api.http.post('//evil.example/collect', {})).rejects.toThrow('must start with /api/');
|
||||
});
|
||||
|
||||
it('rejects absolute URLs to other origins', async () => {
|
||||
const api = makeApi();
|
||||
await expect(api.http.post('https://evil.example/steal', {})).rejects.toThrow('must start with /api/');
|
||||
});
|
||||
|
||||
it('rejects paths not under /api/', async () => {
|
||||
const api = makeApi();
|
||||
await expect(api.http.post('/other/path', {})).rejects.toThrow('must start with /api/');
|
||||
});
|
||||
|
||||
it('rejects paths that use backslash to bypass the check', async () => {
|
||||
const api = makeApi();
|
||||
await expect(api.http.post('/api/\\@evil.example', {})).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws without http:post permission', async () => {
|
||||
const api = makeApi([]);
|
||||
await expect(api.http.post('/api/jitsi', {})).rejects.toThrow('lacks permission');
|
||||
});
|
||||
|
||||
it('accepts a valid /api/ path', async () => {
|
||||
const api = makeApi();
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ url: 'https://meet.example.com/room' }),
|
||||
});
|
||||
const result = await api.http.post('/api/jitsi', { eventTitle: 'test' });
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.data).toEqual({ url: 'https://meet.example.com/room' });
|
||||
});
|
||||
});
|
||||
|
||||
+7
-3
@@ -647,8 +647,12 @@ export function createPluginAPI(plugin: InstalledPlugin): PluginAPI {
|
||||
http: {
|
||||
post: async (path: string, body: Record<string, unknown>) => {
|
||||
requirePermission(plugin, 'http:post');
|
||||
if (typeof path !== 'string' || !path.startsWith('/')) {
|
||||
throw new Error('path must be an absolute path starting with /');
|
||||
if (typeof path !== 'string' || !path.startsWith('/api/')) {
|
||||
throw new Error('path must start with /api/');
|
||||
}
|
||||
const url = new URL(path, globalThis.location.origin);
|
||||
if (url.origin !== globalThis.location.origin) {
|
||||
throw new Error('path must resolve to the same origin');
|
||||
}
|
||||
const { client } = useAuthStore.getState();
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
@@ -656,7 +660,7 @@ export function createPluginAPI(plugin: InstalledPlugin): PluginAPI {
|
||||
headers['Authorization'] = client.getAuthHeader();
|
||||
headers['X-JMAP-Username'] = client.getUsername();
|
||||
}
|
||||
const res = await fetch(path, {
|
||||
const res = await fetch(url.pathname + url.search, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
|
||||
Reference in New Issue
Block a user