0
0
Fork 0
mirror of https://github.com/nextcloud/server.git synced 2025-03-12 23:47:25 +00:00

fix(theming): Ensure to only send valid URLs to backend

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2025-01-21 16:02:45 +01:00 committed by backportbot[bot]
parent e7e5bec6c6
commit d8593af7f2
2 changed files with 178 additions and 4 deletions
apps/theming/src/mixins/admin
cypress/e2e/theming

View file

@ -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
}
},

View file

@ -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')
})
})