|
3 | 3 | */ |
4 | 4 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
5 | 5 | import { executeUpdateSloOperation } from '@/lib/internal/datadog/operations/update-slo' |
| 6 | +import * as datadogTools from '@/tools/datadog' |
6 | 7 | import { cancelDowntimeTool } from '@/tools/datadog/cancel_downtime' |
7 | 8 | import { createDowntimeTool } from '@/tools/datadog/create_downtime' |
8 | 9 | import { createEventTool } from '@/tools/datadog/create_event' |
@@ -652,3 +653,60 @@ describe('datadog site is validated before it reaches the request host', () => { |
652 | 653 | ).toThrow(/Datadog "site" must be one of/) |
653 | 654 | }) |
654 | 655 | }) |
| 656 | + |
| 657 | +describe('every Datadog tool routes its host through the allowlist', () => { |
| 658 | + /* |
| 659 | + * The first pass guarded only the shared `datadogApiUrl`; ten tools built the |
| 660 | + * host inline from `params.site` and bypassed it entirely. Sweeping the registry |
| 661 | + * rather than listing tools means a new tool that reintroduces an inline builder |
| 662 | + * fails here instead of shipping an unguarded credentialed request. |
| 663 | + */ |
| 664 | + const REQUIRED = { |
| 665 | + monitorId: '1', |
| 666 | + downtimeId: '1', |
| 667 | + dashboardId: 'abc-def-ghi', |
| 668 | + incidentId: '1', |
| 669 | + sloId: 'abc', |
| 670 | + signalId: 'abc', |
| 671 | + testId: 'abc', |
| 672 | + publicId: 'abc', |
| 673 | + resultId: 'abc', |
| 674 | + query: 'x', |
| 675 | + from: '1', |
| 676 | + to: '2', |
| 677 | + logs: '[]', |
| 678 | + metrics: '[]', |
| 679 | + series: '[]', |
| 680 | + title: 't', |
| 681 | + text: 't', |
| 682 | + name: 'n', |
| 683 | + type: 'metric alert', |
| 684 | + scope: '*', |
| 685 | + start: '1', |
| 686 | + end: '2', |
| 687 | + testIds: 'a', |
| 688 | + } |
| 689 | + |
| 690 | + it('rejects an attacker-chosen site in every tool that builds a URL', () => { |
| 691 | + const builders = Object.values(datadogTools).filter( |
| 692 | + (tool) => typeof tool?.request?.url === 'function' |
| 693 | + ) |
| 694 | + expect(builders.length).toBeGreaterThan(20) |
| 695 | + |
| 696 | + const unguarded: string[] = [] |
| 697 | + for (const tool of builders) { |
| 698 | + const params = { ...REQUIRED, apiKey: 'k', applicationKey: 'a', site: 'evil.com' } |
| 699 | + try { |
| 700 | + const url = String((tool.request as { url: (p: unknown) => string }).url(params)) |
| 701 | + if (!/^https:\/\/(api|http-intake\.logs)\.(datadoghq\.com|datadoghq\.eu)/.test(url)) { |
| 702 | + unguarded.push(`${tool.id} -> ${url}`) |
| 703 | + } |
| 704 | + } catch (error) { |
| 705 | + if (!/Datadog "site" must be one of/.test(String(error))) { |
| 706 | + unguarded.push(`${tool.id} -> ${String(error)}`) |
| 707 | + } |
| 708 | + } |
| 709 | + } |
| 710 | + expect(unguarded).toEqual([]) |
| 711 | + }) |
| 712 | +}) |
0 commit comments