Skip to content

Commit 2355709

Browse files
committed
fix(http-request): prevent uncaught exceptions in async hooks
This PR fixes several issues that can cause uncaught exceptions and crash Node-RED: 1. Fixed typo: `toLowercase()` -> `toLowerCase()` in getHeaderValue() 2. Added try-catch to beforeRequest hook 3. Added try-catch to beforeRedirect hook 4. Added try-catch to afterResponse hook (digest auth) 5. Added input validation to extractCookies() with array check 6. Added input validation to buildDigestHeader() for nonce/realm These changes ensure that malformed responses or invalid data from servers don't crash the entire Node-RED runtime. Fixes: Uncaught exceptions in HTTP request node
1 parent 6a75a08 commit 2355709

1 file changed

Lines changed: 83 additions & 45 deletions

File tree

packages/node_modules/@node-red/nodes/core/network/21-httprequest.js

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ in your Node-RED user directory (${RED.settings.userDir}).
149149
* @return {any} value
150150
*/
151151
const getHeaderValue = (headersObject, name) => {
152-
const asLowercase = name.toLowercase();
152+
const asLowercase = name.toLowerCase();
153153
return headersObject[Object.keys(headersObject).find(k => k.toLowerCase() === asLowercase)];
154154
}
155155
this.count = 0;
@@ -256,34 +256,42 @@ in your Node-RED user directory (${RED.settings.userDir}).
256256
opts.hooks = {
257257
beforeRequest: [
258258
options => {
259-
// Whilst HTTP headers are meant to be case-insensitive,
260-
// in the real world, there are servers that aren't so compliant.
261-
// GOT will lower case all headers given a chance, so we need
262-
// to restore the case of any headers the user has set.
263-
Object.keys(options.headers).forEach(h => {
264-
if (originalHeaderMap[h] && originalHeaderMap[h] !== h) {
265-
options.headers[originalHeaderMap[h]] = options.headers[h];
266-
delete options.headers[h];
259+
try {
260+
// Whilst HTTP headers are meant to be case-insensitive,
261+
// in the real world, there are servers that aren't so compliant.
262+
// GOT will lower case all headers given a chance, so we need
263+
// to restore the case of any headers the user has set.
264+
Object.keys(options.headers).forEach(h => {
265+
if (originalHeaderMap[h] && originalHeaderMap[h] !== h) {
266+
options.headers[originalHeaderMap[h]] = options.headers[h];
267+
delete options.headers[h];
268+
}
269+
})
270+
if (node.insecureHTTPParser) {
271+
// Setting the property under _unixOptions as pretty
272+
// much the only hack available to get got to apply
273+
// a core http option it doesn't think we should be
274+
// allowed to set
275+
options._unixOptions = { ...options.unixOptions, insecureHTTPParser: true }
267276
}
268-
})
269-
if (node.insecureHTTPParser) {
270-
// Setting the property under _unixOptions as pretty
271-
// much the only hack available to get got to apply
272-
// a core http option it doesn't think we should be
273-
// allowed to set
274-
options._unixOptions = { ...options.unixOptions, insecureHTTPParser: true }
277+
} catch (err) {
278+
node.warn("Error in beforeRequest hook: " + err.message);
275279
}
276280
}
277281
],
278282
beforeRedirect: [
279283
(options, response) => {
280-
let redirectInfo = {
281-
location: response.headers.location
282-
}
283-
if (response.headers.hasOwnProperty('set-cookie')) {
284-
redirectInfo.cookies = extractCookies(response.headers['set-cookie']);
284+
try {
285+
let redirectInfo = {
286+
location: response.headers.location
287+
}
288+
if (response.headers.hasOwnProperty('set-cookie')) {
289+
redirectInfo.cookies = extractCookies(response.headers['set-cookie']);
290+
}
291+
redirectList.push(redirectInfo)
292+
} catch (err) {
293+
node.warn("Error processing redirect: " + err.message);
285294
}
286-
redirectList.push(redirectInfo)
287295
}
288296
]
289297
}
@@ -422,25 +430,30 @@ in your Node-RED user directory (${RED.settings.userDir}).
422430
let digestCreds = this.credentials;
423431
let sentCreds = false;
424432
opts.hooks.afterResponse = [(response, retry) => {
425-
if (response.statusCode === 401) {
426-
if (sentCreds) {
427-
return response
428-
}
429-
const requestUrl = new URL(response.request.requestUrl);
430-
const options = { headers: {} }
431-
const normalisedHeaders = {};
432-
Object.keys(response.headers).forEach(k => {
433-
normalisedHeaders[k.toLowerCase()] = response.headers[k]
434-
})
435-
if (normalisedHeaders['www-authenticate']) {
436-
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname + requestUrl.search, normalisedHeaders['www-authenticate'])
437-
options.headers.Authorization = authHeader;
433+
try {
434+
if (response.statusCode === 401) {
435+
if (sentCreds) {
436+
return response
437+
}
438+
const requestUrl = new URL(response.request.requestUrl);
439+
const options = { headers: {} }
440+
const normalisedHeaders = {};
441+
Object.keys(response.headers).forEach(k => {
442+
normalisedHeaders[k.toLowerCase()] = response.headers[k]
443+
})
444+
if (normalisedHeaders['www-authenticate']) {
445+
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname + requestUrl.search, normalisedHeaders['www-authenticate'])
446+
options.headers.Authorization = authHeader;
447+
}
448+
// response.request.options.merge(options)
449+
sentCreds = true;
450+
return retry(options);
438451
}
439-
// response.request.options.merge(options)
440-
sentCreds = true;
441-
return retry(options);
452+
return response
453+
} catch (err) {
454+
node.warn("Digest authentication failed: " + err.message);
455+
return response;
442456
}
443-
return response
444457
}];
445458
} else if (this.authType === "bearer") {
446459
opts.headers.Authorization = `Bearer ${this.credentials.password||""}`
@@ -720,13 +733,29 @@ in your Node-RED user directory (${RED.settings.userDir}).
720733

721734
function extractCookies(setCookie) {
722735
var cookies = {};
736+
if (!Array.isArray(setCookie)) {
737+
return cookies;
738+
}
723739
setCookie.forEach(function(c) {
724-
var parsedCookie = cookie.parse(c);
725-
var eq_idx = c.indexOf('=');
726-
var key = c.substr(0, eq_idx).trim()
727-
parsedCookie.value = parsedCookie[key];
728-
delete parsedCookie[key];
729-
cookies[key] = parsedCookie;
740+
try {
741+
if (typeof c !== 'string') {
742+
return;
743+
}
744+
var parsedCookie = cookie.parse(c);
745+
var eq_idx = c.indexOf('=');
746+
if (eq_idx === -1) {
747+
return;
748+
}
749+
var key = c.substr(0, eq_idx).trim()
750+
if (!key) {
751+
return;
752+
}
753+
parsedCookie.value = parsedCookie[key];
754+
delete parsedCookie[key];
755+
cookies[key] = parsedCookie;
756+
} catch (err) {
757+
// Skip malformed cookies
758+
}
730759
});
731760
return cookies;
732761
}
@@ -778,6 +807,9 @@ in your Node-RED user directory (${RED.settings.userDir}).
778807
}
779808

780809
function buildDigestHeader(user, pass, method, path, authHeader) {
810+
if (!authHeader || typeof authHeader !== 'string') {
811+
throw new Error("Invalid or missing WWW-Authenticate header");
812+
}
781813
var challenge = {}
782814
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
783815
for (;;) {
@@ -787,6 +819,12 @@ in your Node-RED user directory (${RED.settings.userDir}).
787819
}
788820
challenge[match[1]] = match[2] || match[3]
789821
}
822+
if (!challenge.nonce) {
823+
throw new Error("Invalid digest challenge: missing nonce");
824+
}
825+
if (!challenge.realm) {
826+
throw new Error("Invalid digest challenge: missing realm");
827+
}
790828
var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'
791829
var nc = qop && '00000001'
792830
var cnonce = qop && uuid().replace(/-/g, '')

0 commit comments

Comments
 (0)