1
0
mirror of https://github.com/NginxProxyManager/nginx-proxy-manager.git synced 2025-07-29 19:01:13 +03:00

Fix CVE-2024-46256 and CVE-2024-46257

- Schema validate against bad domain characters
- Integration test for CVE POC examples
- Cypress rewrite of plugins for file upload
This commit is contained in:
Jamie Curnow
2024-10-11 11:21:22 +10:00
parent 7c97516de6
commit c39d5433bc
19 changed files with 358 additions and 180 deletions

View File

@ -0,0 +1,50 @@
/// <reference types="Cypress" />
describe('Certificates endpoints', () => {
let token;
before(() => {
cy.getToken().then((tok) => {
token = tok;
});
});
it('Validate custom certificate', function() {
cy.task('backendApiPostFiles', {
token: token,
path: '/api/nginx/certificates/validate',
files: {
certificate: 'test.example.com.pem',
certificate_key: 'test.example.com-key.pem',
},
}).then((data) => {
cy.validateSwaggerSchema('post', 200, '/nginx/certificates/validate', data);
expect(data).to.have.property('certificate');
expect(data).to.have.property('certificate_key');
});
});
it('Request Certificate - CVE-2024-46256/CVE-2024-46257', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/nginx/certificates',
data: {
domain_names: ['test.com"||echo hello-world||\\\\n test.com"'],
meta: {
dns_challenge: false,
letsencrypt_agree: true,
letsencrypt_email: 'admin@example.com',
},
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');
});
});
});

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd
qACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w
rbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge
Yz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ
oxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z
Eo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X
zGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU
ia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6
YHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe
a0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu
W0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw
o72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW
H8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+
N+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh
ELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU
MDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31
qjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq
cMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9
vMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO
utTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V
g0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1
mJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq
YatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8
EQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk
8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM
RrKmPK/msHKK/sVHiL+NFqo=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw
gZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1
cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD
DDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu
b3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe
bWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93
QEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu
GBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+
2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU
QJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB
Ia5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE
OPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G
A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB
/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t
MA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy
l8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s
VXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn
ATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt
IImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u
m+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV
XxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp
1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw
hp8bJUp/QN7pnOVCDbjTQ+HVMXw=
-----END CERTIFICATE-----

View File

@ -1,9 +1,14 @@
const logger = require('./logger');
const restler = require('@jc21/restler');
const axios = require('axios').default;
const BackendApi = function(config, token) {
this.config = config;
this.token = token;
this.axios = axios.create({
baseURL: config.baseUrl,
timeout: 5000,
});
};
/**
@ -14,129 +19,114 @@ BackendApi.prototype.setToken = function(token) {
};
/**
* @param {bool} returnOnError
*/
BackendApi.prototype._prepareOptions = function(returnOnError) {
let options = {
headers: {
Accept: 'application/json'
}
}
if (this.token) {
options.headers.Authorization = 'Bearer ' + this.token;
}
if (returnOnError) {
options.validateStatus = function () {
return true;
}
}
return options;
};
/**
* @param {*} response
* @param {function} resolve
* @param {function} reject
* @param {bool} returnOnError
*/
BackendApi.prototype._handleResponse = function(response, resolve, reject, returnOnError) {
logger('Response data:', response.data);
if (!returnOnError && typeof response.data === 'object' && typeof response.data.error === 'object') {
if (typeof response.data === 'object' && typeof response.data.error === 'object' && typeof response.data.error.message !== 'undefined') {
reject(new Error(response.data.error.code + ': ' + response.data.error.message));
} else {
reject(new Error('Error ' + response.status));
}
} else {
resolve(response.data);
}
};
/**
* @param {*} err
* @param {function} resolve
* @param {function} reject
* @param {bool} returnOnError
*/
BackendApi.prototype._handleError = function(err, resolve, reject, returnOnError) {
logger('Axios Error:', err);
if (returnOnError) {
resolve(typeof err.response.data !== 'undefined' ? err.response.data : err);
} else {
reject(err);
}
};
/**
* @param {string} method
* @param {string} path
* @param {bool} [returnOnError]
* @param {*} [data]
* @returns {Promise<object>}
*/
BackendApi.prototype.get = function(path, returnOnError) {
BackendApi.prototype.request = function (method, path, returnOnError, data) {
logger(method.toUpperCase(), this.config.baseUrl + path);
const options = this._prepareOptions(returnOnError);
return new Promise((resolve, reject) => {
let headers = {
Accept: 'application/json'
};
if (this.token) {
headers.Authorization = 'Bearer ' + this.token;
let opts = {
method: method,
url: path,
...options
}
if (data !== undefined && data !== null) {
opts.data = data;
}
logger('GET ', this.config.baseUrl + path);
restler
.get(this.config.baseUrl + path, {
headers: headers,
this.axios(opts)
.then((response) => {
this._handleResponse(response, resolve, reject, returnOnError);
})
.on('complete', function(data, response) {
logger('Response data:', data);
if (!returnOnError && data instanceof Error) {
reject(data);
} else if (!returnOnError && response.statusCode != 200) {
if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
reject(new Error(data.error.code + ': ' + data.error.message));
} else {
reject(new Error('Error ' + response.statusCode));
}
} else {
resolve(data);
}
.catch((err) => {
this._handleError(err, resolve, reject, returnOnError);
});
});
};
/**
* @param {string} path
* @param {form} form
* @param {bool} [returnOnError]
* @returns {Promise<object>}
*/
BackendApi.prototype.delete = function(path, returnOnError) {
BackendApi.prototype.postForm = function (path, form, returnOnError) {
logger('POST', this.config.baseUrl + path);
const options = this._prepareOptions(returnOnError);
return new Promise((resolve, reject) => {
let headers = {
Accept: 'application/json'
};
if (this.token) {
headers.Authorization = 'Bearer ' + this.token;
const opts = {
...options,
...form.getHeaders(),
}
logger('DELETE ', this.config.baseUrl + path);
restler
.del(this.config.baseUrl + path, {
headers: headers,
this.axios.post(path, form, opts)
.then((response) => {
this._handleResponse(response, resolve, reject, returnOnError);
})
.on('complete', function(data, response) {
logger('Response data:', data);
if (!returnOnError && data instanceof Error) {
reject(data);
} else if (!returnOnError && response.statusCode != 200) {
if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
reject(new Error(data.error.code + ': ' + data.error.message));
} else {
reject(new Error('Error ' + response.statusCode));
}
} else {
resolve(data);
}
.catch((err) => {
this._handleError(err, resolve, reject, returnOnError);
});
});
};
/**
* @param {string} path
* @param {object} data
* @param {bool} [returnOnError]
* @returns {Promise<object>}
*/
BackendApi.prototype.postJson = function(path, data, returnOnError) {
logger('POST ', this.config.baseUrl + path);
return this._putPostJson('postJson', path, data, returnOnError);
};
/**
* @param {string} path
* @param {object} data
* @param {bool} [returnOnError]
* @returns {Promise<object>}
*/
BackendApi.prototype.putJson = function(path, data, returnOnError) {
logger('PUT ', this.config.baseUrl + path);
return this._putPostJson('putJson', path, data, returnOnError);
};
/**
* @param {string} path
* @param {object} data
* @param {bool} [returnOnError]
* @returns {Promise<object>}
*/
BackendApi.prototype._putPostJson = function(fn, path, data, returnOnError) {
return new Promise((resolve, reject) => {
restler[fn](this.config.baseUrl + path, data, {
headers: {
Accept: 'application/json',
Authorization: 'Bearer ' + this.token,
},
}).on('complete', function(data, response) {
logger('Response data:', data);
if (!returnOnError && data instanceof Error) {
reject(data);
} else if (!returnOnError && (response.statusCode < 200 || response.statusCode >= 300)) {
if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
reject(new Error(data.error.code + ': ' + data.error.message));
} else {
reject(new Error('Error ' + response.statusCode));
}
} else {
resolve(data);
}
});
});
};
module.exports = BackendApi;

View File

@ -1,8 +1,9 @@
const fs = require('fs');
const FormData = require('form-data');
const logger = require('./logger');
const Client = require('./client');
module.exports = function (config) {
logger('Client Ready using', config.baseUrl);
return {
@ -17,7 +18,7 @@ module.exports = function (config) {
backendApiGet: (options) => {
const api = new Client(config);
api.setToken(options.token);
return api.get(options.path, options.returnOnError || false);
return api.request('get', options.path, options.returnOnError || false);
},
/**
@ -31,7 +32,26 @@ module.exports = function (config) {
backendApiPost: (options) => {
const api = new Client(config);
api.setToken(options.token);
return api.postJson(options.path, options.data, options.returnOnError || false);
return api.request('post', options.path, options.returnOnError || false, options.data);
},
/**
* @param {object} options
* @param {string} options.token JWT
* @param {string} options.path API path
* @param {object} options.files
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
* @returns {string}
*/
backendApiPostFiles: (options) => {
const api = new Client(config);
api.setToken(options.token);
const form = new FormData();
for (let [key, value] of Object.entries(options.files)) {
form.append(key, fs.createReadStream(config.fixturesFolder + '/' + value));
}
return api.postForm(options.path, form, options.returnOnError || false);
},
/**
@ -45,7 +65,7 @@ module.exports = function (config) {
backendApiPut: (options) => {
const api = new Client(config);
api.setToken(options.token);
return api.putJson(options.path, options.data, options.returnOnError || false);
return api.request('put', options.path, options.returnOnError || false, options.data);
},
/**
@ -58,7 +78,7 @@ module.exports = function (config) {
backendApiDelete: (options) => {
const api = new Client(config);
api.setToken(options.token);
return api.delete(options.path, options.returnOnError || false);
return api.request('delete', options.path, options.returnOnError || false);
}
};
};
};