Closed valeriansaliou closed 3 weeks ago
const { performance } = require('perf_hooks');
// Constants
const CHAR_CR = 0x0d;
const CHAR_LF = 0x0a;
const CHAR_SPACE = 0x20;
const CHAR_TAB = 0x09;
function patchedFixLineBuffer(line) {
// Allocate max possible size
const resultLine = Buffer.alloc(line.length * 2);
let writeIndex = resultLine.length - 1;
let nonWspFound = false;
let prevWsp = false;
for (let i = line.length - 1; i >= 0; i--) {
if (line[i] === CHAR_LF) {
resultLine[writeIndex--] = CHAR_LF;
if (i === 0 || line[i - 1] !== CHAR_CR) {
resultLine[writeIndex--] = CHAR_CR;
}
continue;
}
if (line[i] === CHAR_CR) {
resultLine[writeIndex--] = CHAR_CR;
continue;
}
if (line[i] === CHAR_SPACE || line[i] === CHAR_TAB) {
if (nonWspFound) {
prevWsp = true;
}
continue;
}
if (prevWsp) {
resultLine[writeIndex--] = CHAR_SPACE;
prevWsp = false;
}
nonWspFound = true;
resultLine[writeIndex--] = line[i];
}
if (prevWsp && nonWspFound) {
resultLine[writeIndex--] = CHAR_SPACE;
}
return resultLine.slice(writeIndex + 1);
}
// The vulnerable fixLineBuffer function
function fixLineBuffer(line) {
let resultLine = [];
let nonWspFound = false;
let prevWsp = false;
for (let i = line.length - 1; i >= 0; i--) {
if (line[i] === CHAR_LF) {
resultLine.unshift(line[i]);
if (i === 0 || line[i - 1] !== CHAR_CR) {
resultLine.unshift(CHAR_CR);
}
continue;
}
if (line[i] === CHAR_CR) {
resultLine.unshift(line[i]);
continue;
}
if (line[i] === CHAR_SPACE || line[i] === CHAR_TAB) {
if (nonWspFound) {
prevWsp = true;
}
continue;
}
if (prevWsp) {
resultLine.unshift(CHAR_SPACE);
prevWsp = false;
}
nonWspFound = true;
resultLine.unshift(line[i]);
}
if (prevWsp && nonWspFound) {
resultLine.unshift(CHAR_SPACE);
}
return Buffer.from(resultLine);
}
// Function to generate a test line
function generateTestLine(length) {
let line = Buffer.alloc(length);
for (let i = 0; i < length; i++) {
line[i] = i % 2 === 0 ? CHAR_SPACE : 97 + (i % 26); // alternating space and lowercase letters
}
return line;
}
// Function to run the test
function runTest(length) {
const testLine = generateTestLine(length);
let start = performance.now();
patchedFixLineBuffer(testLine);
let end = performance.now();
console.log(`PATCHED Line length: ${length}, Time taken: ${(end - start).toFixed(2)} ms`);
start = performance.now();
fixLineBuffer(testLine);
end = performance.now();
console.log(`CURRENT Line length: ${length}, Time taken: ${(end - start).toFixed(2)} ms`);
}
// Run tests with increasing line lengths
console.log("Starting fixLineBuffer vulnerability test...");
for (let length = 1000; length <= 1000000; length *= 10) {
runTest(length);
}
// Test with an extremely long line to demonstrate potential DoS
console.log("\nTesting with an extremely long line:");
runTest(10000000); // 10 million characters
Here is a script that reproduce the problem
Hello!
Encountered CPU stuck at 100% on all our inbound email microservices fleet this night and morning, for a repeated number of times after manual NodeJS process restarts probably due to SMTP delivery retries.
The issue seems to be a DOS, which was traced to origin in
mailauth
DKIM module after performing a Node profile (--prof
), here's the trace extract from the processed profile:When tracing the code, there appears to be some O(n^2) algorithm involved here (no Regex, this does not appear to be a ReDOS to me): https://github.com/postalsys/mailauth/blob/master/lib/dkim/body/relaxed.js#L212
I unfortunately do not have the original mail data which triggered this possible DOS vulnerability, since we do not keep a record of emails that went through before processing them. I'm trying to obtain the email content now.