mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-31 07:43:33 +00: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:
		
							
								
								
									
										50
									
								
								test/cypress/e2e/api/Certificates.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								test/cypress/e2e/api/Certificates.cy.js
									
									
									
									
									
										Normal 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'); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										28
									
								
								test/cypress/fixtures/test.example.com-key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								test/cypress/fixtures/test.example.com-key.pem
									
									
									
									
									
										Normal 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----- | ||||
							
								
								
									
										26
									
								
								test/cypress/fixtures/test.example.com.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								test/cypress/fixtures/test.example.com.pem
									
									
									
									
									
										Normal 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----- | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
| 		} | ||||
| 	}; | ||||
| }; | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user