Skip to content

Commit 4d043d8

Browse files
committed
test(recommendations): assert hostile payload escaping in cascading filter popover
Add a regression test that verifies a service value containing an HTML injection string (<script>alert(1)</script>) is stored in dataset and rendered via textContent -- never interpreted as markup -- when the cascading filter popover builds its checkbox list.
1 parent ac25809 commit 4d043d8

1 file changed

Lines changed: 38 additions & 0 deletions

File tree

‎frontend/src/__tests__/recommendations.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2895,6 +2895,44 @@ describe('Bundle B: column header filter triggers', () => {
28952895
// the alwaysInclude set pins values from the column's own active filter.
28962896
expect(values).toContain('ec2');
28972897
});
2898+
2899+
test('hostile payload in service value is not interpreted as HTML in the popover (XSS guard)', async () => {
2900+
// A recommendation whose `service` field contains an HTML injection string
2901+
// must not be parsed as markup in the filter popover -- the value goes through
2902+
// textContent and dataset, never innerHTML.
2903+
const hostileService = '<script>alert(1)</script>';
2904+
const recsWithHostile = [
2905+
{ id: 'r-aws-ec2', provider: 'aws', cloud_account_id: 'a1', service: hostileService, resource_type: 't3.medium', region: 'us-east-1', count: 1, term: 1, savings: 100, upfront_cost: 500 },
2906+
{ id: 'r-aws-rds', provider: 'aws', cloud_account_id: 'a1', service: 'rds', resource_type: 'db.t3', region: 'us-east-1', count: 1, term: 1, savings: 80, upfront_cost: 400 },
2907+
];
2908+
(api.getRecommendations as jest.Mock).mockResolvedValue({
2909+
summary: {},
2910+
recommendations: recsWithHostile,
2911+
regions: [],
2912+
});
2913+
(state.getRecommendations as jest.Mock).mockReturnValue(recsWithHostile);
2914+
(state.getVisibleRecommendations as jest.Mock).mockReturnValue(recsWithHostile);
2915+
(state.getRecommendationsColumnFilters as jest.Mock).mockReturnValue({});
2916+
await loadRecommendations();
2917+
const serviceBtn = document.querySelector<HTMLButtonElement>('th .column-filter-btn[data-column="service"]');
2918+
serviceBtn?.click();
2919+
2920+
// The popover must not contain a parsed <script> element.
2921+
const popover = document.querySelector('.column-filter-popover');
2922+
expect(popover).not.toBeNull();
2923+
expect(popover!.querySelector('script')).toBeNull();
2924+
2925+
// The raw payload must appear as a literal data-value attribute, not interpreted HTML.
2926+
const hostileCb = Array.from(
2927+
document.querySelectorAll<HTMLInputElement>('.column-filter-popover .column-filter-item input[type="checkbox"]'),
2928+
).find((cb) => cb.dataset['value'] === hostileService);
2929+
expect(hostileCb).not.toBeUndefined();
2930+
2931+
// The label span renders the payload as literal text, not HTML.
2932+
const labelSpan = hostileCb!.closest('label')?.querySelector('span');
2933+
expect(labelSpan?.textContent).toBe(hostileService);
2934+
expect(labelSpan?.innerHTML).not.toContain('<script>');
2935+
});
28982936
});
28992937
});
29002938

0 commit comments

Comments
 (0)