aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2025-01-21 16:02:45 +0100
committerbackportbot[bot] <backportbot[bot]@users.noreply.github.com>2025-01-27 17:57:41 +0000
commitd8593af7f22e3022ad08a07cbf6cd5c9e1dd6865 (patch)
treed876f01406f0fbf44d1066ba5733bae009ca8bc1
parente7e5bec6c6e229d3aa069a24cc46488d7d4e5344 (diff)
downloadnextcloud-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.js39
-rw-r--r--cypress/e2e/theming/admin-settings_urls.cy.ts143
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')
+ })
+
+})