diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-01-21 16:02:45 +0100 |
---|---|---|
committer | backportbot[bot] <backportbot[bot]@users.noreply.github.com> | 2025-01-27 17:57:41 +0000 |
commit | d8593af7f22e3022ad08a07cbf6cd5c9e1dd6865 (patch) | |
tree | d876f01406f0fbf44d1066ba5733bae009ca8bc1 | |
parent | e7e5bec6c6e229d3aa069a24cc46488d7d4e5344 (diff) | |
download | nextcloud-server-d8593af7f22e3022ad08a07cbf6cd5c9e1dd6865.tar.gz nextcloud-server-d8593af7f22e3022ad08a07cbf6cd5c9e1dd6865.zip |
fix(theming): Ensure to only send valid URLs to backend
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r-- | apps/theming/src/mixins/admin/TextValueMixin.js | 39 | ||||
-rw-r--r-- | cypress/e2e/theming/admin-settings_urls.cy.ts | 143 |
2 files changed, 178 insertions, 4 deletions
diff --git a/apps/theming/src/mixins/admin/TextValueMixin.js b/apps/theming/src/mixins/admin/TextValueMixin.js index 4cce8bb301a..d138952d16a 100644 --- a/apps/theming/src/mixins/admin/TextValueMixin.js +++ b/apps/theming/src/mixins/admin/TextValueMixin.js @@ -38,25 +38,56 @@ export default { data() { return { + /** @type {string|boolean} */ localValue: this.value, } }, + computed: { + valueToPost() { + if (this.type === 'url') { + // if this is already encoded just make sure there is no doublequote (HTML XSS) + // otherwise simply URL encode + return this.isUrlEncoded(this.localValue) + ? this.localValue.replaceAll('"', '%22') + : encodeURI(this.localValue) + } + // Convert boolean to string as server expects string value + if (typeof this.localValue === 'boolean') { + return this.localValue ? 'yes' : 'no' + } + return this.localValue + }, + }, + methods: { + /** + * Check if URL is percent-encoded + * @param {string} url The URL to check + * @return {boolean} + */ + isUrlEncoded(url) { + try { + return decodeURI(url) !== url + } catch { + return false + } + }, + async save() { this.reset() const url = generateUrl('/apps/theming/ajax/updateStylesheet') - // Convert boolean to string as server expects string value - const valueToPost = this.localValue === true ? 'yes' : this.localValue === false ? 'no' : this.localValue + try { await axios.post(url, { setting: this.name, - value: valueToPost, + value: this.valueToPost, }) this.$emit('update:value', this.localValue) this.handleSuccess() } catch (e) { - this.errorMessage = e.response.data.data?.message + console.error('Failed to save changes', e) + this.errorMessage = e.response?.data.data?.message } }, diff --git a/cypress/e2e/theming/admin-settings_urls.cy.ts b/cypress/e2e/theming/admin-settings_urls.cy.ts new file mode 100644 index 00000000000..46bae7901c4 --- /dev/null +++ b/cypress/e2e/theming/admin-settings_urls.cy.ts @@ -0,0 +1,143 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { User } from '@nextcloud/cypress' + +const admin = new User('admin', 'admin') + +describe('Admin theming: Setting custom project URLs', function() { + this.beforeEach(() => { + // Just in case previous test failed + cy.resetAdminTheming() + cy.login(admin) + cy.visit('/settings/admin/theming') + cy.intercept('POST', '**/apps/theming/ajax/updateStylesheet').as('updateTheming') + }) + + it('Setting the web link', () => { + cy.findByRole('textbox', { name: /web link/i }) + .and('have.attr', 'type', 'url') + .as('input') + .scrollIntoView() + cy.get('@input') + .should('be.visible') + .type('{selectAll}http://example.com/path?query#fragment{enter}') + + cy.wait('@updateTheming') + + cy.logout() + + cy.visit('/') + cy.contains('a', 'Nextcloud') + .should('be.visible') + .and('have.attr', 'href', 'http://example.com/path?query#fragment') + }) + + it('Setting the legal notice link', () => { + cy.findByRole('textbox', { name: /legal notice link/i }) + .should('exist') + .and('have.attr', 'type', 'url') + .as('input') + .scrollIntoView() + cy.get('@input') + .type('http://example.com/path?query#fragment{enter}') + + cy.wait('@updateTheming') + + cy.logout() + + cy.visit('/') + cy.contains('a', /legal notice/i) + .should('be.visible') + .and('have.attr', 'href', 'http://example.com/path?query#fragment') + }) + + it('Setting the privacy policy link', () => { + cy.findByRole('textbox', { name: /privacy policy link/i }) + .should('exist') + .as('input') + .scrollIntoView() + cy.get('@input') + .should('have.attr', 'type', 'url') + .type('http://privacy.local/path?query#fragment{enter}') + + cy.wait('@updateTheming') + + cy.logout() + + cy.visit('/') + cy.contains('a', /privacy policy/i) + .should('be.visible') + .and('have.attr', 'href', 'http://privacy.local/path?query#fragment') + }) + +}) + +describe('Admin theming: Web link corner cases', function() { + this.beforeEach(() => { + // Just in case previous test failed + cy.resetAdminTheming() + cy.login(admin) + cy.visit('/settings/admin/theming') + cy.intercept('POST', '**/apps/theming/ajax/updateStylesheet').as('updateTheming') + }) + + it('Already URL encoded', () => { + cy.findByRole('textbox', { name: /web link/i }) + .and('have.attr', 'type', 'url') + .as('input') + .scrollIntoView() + cy.get('@input') + .should('be.visible') + .type('{selectAll}http://example.com/%22path%20with%20space%22{enter}') + + cy.wait('@updateTheming') + + cy.logout() + + cy.visit('/') + cy.contains('a', 'Nextcloud') + .should('be.visible') + .and('have.attr', 'href', 'http://example.com/%22path%20with%20space%22') + }) + + it('URL with double quotes', () => { + cy.findByRole('textbox', { name: /web link/i }) + .and('have.attr', 'type', 'url') + .as('input') + .scrollIntoView() + cy.get('@input') + .should('be.visible') + .type('{selectAll}http://example.com/"path"{enter}') + + cy.wait('@updateTheming') + + cy.logout() + + cy.visit('/') + cy.contains('a', 'Nextcloud') + .should('be.visible') + .and('have.attr', 'href', 'http://example.com/%22path%22') + }) + + it('URL with double quotes and already encoded', () => { + cy.findByRole('textbox', { name: /web link/i }) + .and('have.attr', 'type', 'url') + .as('input') + .scrollIntoView() + cy.get('@input') + .should('be.visible') + .type('{selectAll}http://example.com/"the%20path"{enter}') + + cy.wait('@updateTheming') + + cy.logout() + + cy.visit('/') + cy.contains('a', 'Nextcloud') + .should('be.visible') + .and('have.attr', 'href', 'http://example.com/%22the%20path%22') + }) + +}) |