From 82529d6402b63090592bf34ddc760e9d328c741c Mon Sep 17 00:00:00 2001 From: Alexis Loskoutoff Date: Tue, 13 Oct 2026 18:34:20 +0200 Subject: [PATCH 1/3] Require certificates:create permission to validate certificates --- backend/internal/certificate.js | 8 +++++--- backend/routes/nginx/certificates.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 9a14f5893..9f81b764d 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -545,13 +545,15 @@ const internalCertificate = { /** * Validates that the certs provided are good. - * No access required here, nothing is changed or stored. + * Nothing is changed or stored, but the files go through openssl, so it needs the same access as creating one. * + * @param {Access} access * @param {Object} data * @param {Object} data.files * @returns {Promise} */ - validate: (data) => { + validate: async (access, data) => { + await access.can("certificates:create"); // Put file contents into an object const files = {}; _.map(data.files, (file, name) => { @@ -600,7 +602,7 @@ const internalCertificate = { throw new error.ValidationError("Cannot upload certificates for this type of provider"); } - const validations = await internalCertificate.validate(data); + const validations = await internalCertificate.validate(access, data); if (typeof validations.certificate === "undefined") { throw new error.ValidationError("Certificate file was not provided"); } diff --git a/backend/routes/nginx/certificates.js b/backend/routes/nginx/certificates.js index dea809423..90f279c37 100644 --- a/backend/routes/nginx/certificates.js +++ b/backend/routes/nginx/certificates.js @@ -161,7 +161,7 @@ router } try { - const result = await internalCertificate.validate({ + const result = await internalCertificate.validate(res.locals.access, { files: req.files, }); res.status(200).send(result); From 6acf34626214e59dfca8ac546d8c00af32111d81 Mon Sep 17 00:00:00 2001 From: Alexis Loskoutoff Date: Tue, 13 Oct 2026 18:35:50 +0200 Subject: [PATCH 2/3] Add test for unauthenticated certificate validation --- test/cypress/e2e/api/Certificates.cy.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js index 7d75d40cf..80fc9f395 100644 --- a/test/cypress/e2e/api/Certificates.cy.js +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -35,6 +35,19 @@ describe('Certificates endpoints', () => { }); }); + it('Unauthenticated user cannot validate custom certificate', () => { + cy.task('backendApiPostFiles', { + path: '/api/nginx/certificates/validate', + files: { + certificate: certFile, + certificate_key: keyFile, + }, + returnOnError: true, + }).then((data) => { + expect(data.error.code).to.equal(403); + }); + }); + it('Custom certificate lifecycle', () => { // Create custom cert cy.task('backendApiPost', { From f1bd1574380f3e749dcfcd539c429bcbc6d57292 Mon Sep 17 00:00:00 2001 From: Alexis Loskoutoff Date: Tue, 13 Oct 2026 19:32:53 +0200 Subject: [PATCH 3/3] Add test for certificate validation with view-only permission --- test/cypress/e2e/api/Certificates.cy.js | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js index 80fc9f395..cc197bc36 100644 --- a/test/cypress/e2e/api/Certificates.cy.js +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -48,6 +48,50 @@ describe('Certificates endpoints', () => { }); }); + it('User with certificates view permission cannot validate custom certificate', () => { + const viewer = { + email: 'viewer@example.com', + secret: 'changeme', + }; + + cy.task('backendApiPost', { + token: token, + path: '/api/users', + data: { + name: 'Certificate Viewer', + nickname: 'Viewer', + email: viewer.email, + roles: ['user'], + auth: { + type: 'password', + secret: viewer.secret, + }, + }, + }).then((user) => { + cy.task('backendApiPut', { + token: token, + path: `/api/users/${user.id}/permissions`, + data: { + certificates: 'view', + }, + }); + + cy.getToken(null, {identity: viewer.email, secret: viewer.secret}).then((viewerToken) => { + cy.task('backendApiPostFiles', { + token: viewerToken, + path: '/api/nginx/certificates/validate', + files: { + certificate: certFile, + certificate_key: keyFile, + }, + returnOnError: true, + }).then((data) => { + expect(data.error.code).to.equal(403); + }); + }); + }); + }); + it('Custom certificate lifecycle', () => { // Create custom cert cy.task('backendApiPost', {