mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-11-10 12:28:28 +00:00
Security - Check advanced_config permissions before creating a certificate on host update (proxy, redirection and 404 hosts). A refused request previously requested a Let's Encrypt certificate first, leaving an orphan certificate and using up rate limit. - Only trust X-NPM-Peer-Addr when it comes with a secret generated at container start. The admin nginx sends both headers. Any proxy host or stream forwarding to 127.0.0.1:3000 was also a loopback peer, so it could forge the header and get a fresh rate-limit bucket on every request. Without the secret (dev, or a mounted production.conf), the header is ignored. - Add a login limit per IP across all accounts (30 failed attempts a minute) alongside the per-account one. Before this, one address could spray passwords over any number of accounts. Behind a shared proxy, everyone using that proxy shares this limit. - Stop trusting X-Real-IP from loopback. It was only there for an older version of the rate limiter, and it let a stream host forwarding to a local proxy host forge the client IP and get past IP access lists. Loopback now behaves as it does on master. Correctness - Refuse to delete a certificate that hosts still use (400, with counts per host type). Their configs include its files, and a successful revoke deleted them anyway, which broke nginx -t. Otherwise the certificate is revoked and its files are removed even if the revoke fails. - Let non-admins clear a location's advanced config. Deleting the location was already allowed and has the same effect, and a location deleted and re-added at the same path arrives with an empty config, which was refused. - Clear proxy host grants (meta.proxy_host_ids) when visibility isn't "user" or proxy hosts are hidden, and drop ids of deleted hosts. Before, hidden grants came back when proxy hosts were shown again. The backend enforces this; the permissions modal also stops sending them. - 404 host and redirection host modals now wait for the current user to load, so an admin's advanced config change isn't dropped if they submit before it loads. Tests - NonAdminHosts.cy.js: refused change doesn't create a certificate, location rename and clear rules, grant clearing. - Certificates.cy.js: deleting a certificate that is in use.
241 lines
6.4 KiB
JavaScript
241 lines
6.4 KiB
JavaScript
/// <reference types="cypress" />
|
|
|
|
describe('Certificates endpoints', () => {
|
|
let token;
|
|
let certID;
|
|
|
|
const certFile = 'test.example.com.pem';
|
|
const keyFile = 'test.example.com-key.pem';
|
|
|
|
before(() => {
|
|
cy.createCustomCerts({
|
|
domain: 'test.example.com',
|
|
certFile,
|
|
keyFile,
|
|
})
|
|
|
|
cy.resetUsers();
|
|
cy.getToken().then((tok) => {
|
|
token = tok;
|
|
});
|
|
});
|
|
|
|
it('Validate custom certificate', () => {
|
|
cy.task('backendApiPostFiles', {
|
|
token: token,
|
|
path: '/api/nginx/certificates/validate',
|
|
files: {
|
|
certificate: certFile,
|
|
certificate_key: keyFile,
|
|
},
|
|
}).then((data) => {
|
|
cy.validateSwaggerSchema('post', 200, '/nginx/certificates/validate', data);
|
|
expect(data).to.have.property('certificate');
|
|
expect(data).to.have.property('certificate_key');
|
|
});
|
|
});
|
|
|
|
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('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', {
|
|
token: token,
|
|
path: '/api/nginx/certificates',
|
|
data: {
|
|
provider: "other",
|
|
nice_name: "Test Certificate",
|
|
},
|
|
}).then((data) => {
|
|
cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data);
|
|
expect(data).to.have.property('id');
|
|
certID = data.id;
|
|
|
|
// Upload files
|
|
cy.task('backendApiPostFiles', {
|
|
token: token,
|
|
path: `/api/nginx/certificates/${certID}/upload`,
|
|
files: {
|
|
certificate: certFile,
|
|
certificate_key: keyFile,
|
|
},
|
|
}).then((data) => {
|
|
cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data);
|
|
expect(data).to.have.property('certificate');
|
|
expect(data).to.have.property('certificate_key');
|
|
|
|
// Get all certs
|
|
cy.task('backendApiGet', {
|
|
token: token,
|
|
path: '/api/nginx/certificates?expand=owner'
|
|
}).then((data) => {
|
|
cy.validateSwaggerSchema('get', 200, '/nginx/certificates', data);
|
|
expect(data.length).to.be.greaterThan(0);
|
|
const cert = data.find((c) => c.id === certID);
|
|
expect(cert.meta).to.have.property('certificate');
|
|
expect(cert.meta).to.not.have.property('certificate_key');
|
|
|
|
// Delete cert
|
|
cy.task('backendApiDelete', {
|
|
token: token,
|
|
path: `/api/nginx/certificates/${certID}`
|
|
}).then((data) => {
|
|
cy.validateSwaggerSchema('delete', 200, '/nginx/certificates/{certID}', data);
|
|
expect(data).to.be.equal(true);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('Certificate in use by a host cannot be deleted', () => {
|
|
cy.task('backendApiPost', {
|
|
token: token,
|
|
path: '/api/nginx/certificates',
|
|
data: {
|
|
provider: 'other',
|
|
nice_name: 'In Use Certificate',
|
|
},
|
|
}).then((cert) => {
|
|
cy.task('backendApiPostFiles', {
|
|
token: token,
|
|
path: `/api/nginx/certificates/${cert.id}/upload`,
|
|
files: {
|
|
certificate: certFile,
|
|
certificate_key: keyFile,
|
|
},
|
|
}).then(() => {
|
|
cy.task('backendApiPost', {
|
|
token: token,
|
|
path: '/api/nginx/proxy-hosts',
|
|
data: {
|
|
domain_names: ['cert-in-use.example.com'],
|
|
forward_scheme: 'http',
|
|
forward_host: '1.1.1.1',
|
|
forward_port: 80,
|
|
access_list_id: '0',
|
|
certificate_id: cert.id,
|
|
meta: { dns_challenge: false },
|
|
advanced_config: '',
|
|
locations: [],
|
|
block_exploits: false,
|
|
caching_enabled: false,
|
|
allow_websocket_upgrade: false,
|
|
http2_support: false,
|
|
hsts_enabled: false,
|
|
hsts_subdomains: false,
|
|
ssl_forced: false,
|
|
},
|
|
}).then((host) => {
|
|
cy.task('backendApiDelete', {
|
|
token: token,
|
|
path: `/api/nginx/certificates/${cert.id}`,
|
|
returnOnError: true,
|
|
}).then((data) => {
|
|
cy.validateSwaggerSchema('delete', 400, '/nginx/certificates/{certID}', data);
|
|
expect(data.error.code).to.equal(400);
|
|
expect(data.error.message).to.contain('in use by 1 proxy host');
|
|
});
|
|
|
|
// Still there
|
|
cy.task('backendApiGet', {
|
|
token: token,
|
|
path: `/api/nginx/certificates/${cert.id}`,
|
|
}).then((data) => {
|
|
expect(data.id).to.equal(cert.id);
|
|
});
|
|
|
|
// Once the host no longer uses it, it can be deleted
|
|
cy.task('backendApiDelete', {
|
|
token: token,
|
|
path: `/api/nginx/proxy-hosts/${host.id}`,
|
|
});
|
|
cy.task('backendApiDelete', {
|
|
token: token,
|
|
path: `/api/nginx/certificates/${cert.id}`,
|
|
}).then((data) => {
|
|
expect(data).to.be.equal(true);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('Request Certificate - CVE-2024-46256/CVE-2024-46257', () => {
|
|
cy.task('backendApiPost', {
|
|
token: token,
|
|
path: '/api/nginx/certificates',
|
|
data: {
|
|
domain_names: ['test.com"||echo hello-world||\\\\n test.com"'],
|
|
meta: {
|
|
dns_challenge: false,
|
|
},
|
|
provider: 'letsencrypt',
|
|
},
|
|
returnOnError: true,
|
|
}).then((data) => {
|
|
cy.validateSwaggerSchema('post', 400, '/nginx/certificates', data);
|
|
expect(data).to.have.property('error');
|
|
expect(data.error).to.have.property('message');
|
|
expect(data.error).to.have.property('code');
|
|
expect(data.error.code).to.equal(400);
|
|
expect(data.error.message).to.contain('data/domain_names/0 must match pattern');
|
|
});
|
|
});
|
|
});
|