Skip to content

Commit bc48787

Browse files
Jens Vannerumtdonohue
authored andcommitted
e2e test to proof XSS via metadata directive
1 parent 23fdb11 commit bc48787

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Regression test for a Cross-Site Scripting (XSS) vulnerability that was introduced by
3+
* https://github.com/DSpace/dspace-angular/pull/4776.
4+
*
5+
* This test creates a new item submission whose abstract contains such a payload and verifies that:
6+
* - the payload is NOT executed (no JavaScript side effect happens), and
7+
* - the dangerous `onerror` attribute is stripped from the rendered markup (while safe, surrounding
8+
* markup/tags are preserved),
9+
* when the abstract is displayed via the `[dsMetadata]` directive.
10+
*
11+
*/
12+
describe('Metadata XSS sanitization', () => {
13+
// A classic XSS payload: an image with a broken `src` so that its `onerror` handler fires as soon as
14+
// the browser tries (and fails) to load it. If the payload is not sanitized, `onerror` will run and set
15+
// `window.dsXssExecuted = true`.
16+
const XSS_PAYLOAD = 'XSS Test <img src="x" onerror="window.dsXssExecuted = true;"/>';
17+
const SAFE_TEXT = 'XSS Test';
18+
const UNIQUE_TITLE = `XSS sanitization test item ${Date.now()}`;
19+
20+
/**
21+
* Asserts that the XSS payload has NOT executed on the current page.
22+
*/
23+
function assertXssDidNotExecute(): void {
24+
cy.window().then((win: any) => {
25+
expect(win.dsXssExecuted).to.not.equal(true);
26+
});
27+
}
28+
29+
it('should sanitize a malicious item abstract and not execute injected script when rendered via [dsMetadata]', () => {
30+
cy.visit('/mydspace');
31+
32+
// This page is restricted, so we will be shown the login form. Fill it out & submit.
33+
cy.env(['DSPACE_TEST_SUBMIT_USER', 'DSPACE_TEST_SUBMIT_USER_PASSWORD']).then(({ DSPACE_TEST_SUBMIT_USER, DSPACE_TEST_SUBMIT_USER_PASSWORD }) => {
34+
cy.loginViaForm(DSPACE_TEST_SUBMIT_USER, DSPACE_TEST_SUBMIT_USER_PASSWORD);
35+
});
36+
37+
// Start a submission
38+
cy.get('button[data-test="submission-dropdown"]').click();
39+
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
40+
cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.expose('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
41+
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.expose('DSPACE_TEST_SUBMIT_COLLECTION_NAME')).concat('"]')).click();
42+
43+
// Give the item a unique (safe) title so we can reliably find it again afterward
44+
cy.get('#dc_title', { timeout: 10000 }).type(UNIQUE_TITLE);
45+
46+
// Enter our malicious abstract into the dc.description.abstract field
47+
cy.get('#dc_description_abstract').type(XSS_PAYLOAD);
48+
49+
// Save for Later to persist the (unsanitized, as stored) abstract on the workspace item
50+
cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
51+
52+
// "Save for Later" should send us to MyDSpace
53+
cy.url().should('include', '/mydspace');
54+
// The malicious payload should NOT have executed while the submission form/footer rendered the abstract
55+
assertXssDidNotExecute();
56+
57+
// Close any open notifications, to make sure they don't get in the way of next steps
58+
cy.get('[data-bs-dismiss="alert"]').click({ multiple: true });
59+
60+
// Search for the item we just created via its unique title
61+
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
62+
cy.get('[data-test="search-box"]').type(UNIQUE_TITLE);
63+
cy.get('[data-test="search-button"]').click();
64+
cy.wait('@search-results');
65+
66+
// Find the specific result matching our unique title, and scope all further assertions to it.
67+
cy.contains('[data-test="list-object"]', UNIQUE_TITLE, { timeout: 10000 })
68+
.should('exist')
69+
.as('result');
70+
71+
// The XSS payload must NOT have executed while the [dsMetadata] directive rendered the abstract
72+
assertXssDidNotExecute();
73+
74+
// The abstract should be rendered (via the [dsMetadata] directive) inside a truncatable part
75+
cy.get('@result').find('.item-list-abstract span').first().then(($abstract) => {
76+
// Sanitization removes *dangerous attributes* (like `onerror`), but it does NOT necessarily
77+
// remove the surrounding element itself (e.g. `<img>` is a permitted tag). So we assert that:
78+
// - the safe text content is still present,
79+
// - the `onerror` attribute is gone from the markup entirely,
80+
// - if the `<img>` tag survived sanitization, it has no `onerror` attribute on it.
81+
expect($abstract.text()).to.include(SAFE_TEXT);
82+
expect($abstract.html()).to.not.include('onerror');
83+
84+
const img = $abstract.find('img');
85+
if (img.length > 0) {
86+
// eslint-disable-next-line no-unused-expressions,@typescript-eslint/no-unused-expressions
87+
expect(img.attr('onerror')).to.be.undefined;
88+
}
89+
});
90+
});
91+
});
92+

0 commit comments

Comments
 (0)