Skip to content

Possible out-of-bounds read disclosed over the network in SocketWrap::Send #92

Description

@OvOhao

Possible out-of-bounds read disclosed over the network in SocketWrap::Send

I found a possible OOB read (CWE-125) in SocketWrap::Send. The method takes a JS Buffer plus
an offset and length, both unsigned integers fully controlled by the caller, and forms the
send pointer as node::Buffer::Data(buffer) + offset, then hands data/length to sendto()
— with no check that offset or offset + length stay within the Buffer's actual size. A caller
supplying an offset/length beyond the Buffer bounds makes the kernel read length bytes
starting past (or before, via wrap) the Buffer's backing store, and those out-of-bounds bytes are
transmitted to the destination address (also caller-supplied), disclosing adjacent heap memory
over the wire. Compare CreateChecksum in the same file, which validates offset < length and
new_length <= length, and SetOption, which checks len > Buffer::LengthSend does neither.

File: src/raw.cc

Function: SocketWrap::Send

buffer = Nan::To<Object>(info[0]).ToLocalChecked();
offset = Nan::To<Uint32>(info[1]).ToLocalChecked()->Value();
length = Nan::To<Uint32>(info[2]).ToLocalChecked()->Value();

data = node::Buffer::Data (buffer) + offset;
...
rc = sendto (socket->poll_fd_, data, length, 0,
        (struct sockaddr *) &addr, sizeof (addr));
  1. Only the argument types are validated (Buffer, Uint32, Uint32, String, Function). The
    numeric offset and length values are never bounds-checked against
    node::Buffer::Length(buffer).
  2. data = node::Buffer::Data(buffer) + offset; advances the pointer by an unchecked offset,
    which may already point past the end of the backing store.
  3. sendto(fd, data, length, ...) reads length bytes from data. With offset/length
    chosen beyond the buffer, this reads out-of-bounds heap memory.
  4. The bytes read are sent to the caller-controlled address (info[3]), so the over-read is an
    information disclosure, not merely a crash.

JS trigger (if applicable):

Note: the OOB is not reachable through the public Socket.prototype.send. That wrapper
(index.js) validates the range before calling the binding:

// index.js — Socket.prototype.send
if (length + offset > buffer.length)
    throw new RangeError ("Length and offset must be within the given buffer");

so s.send(buf, 0, 4096, ...) throws in JS and never reaches SocketWrap::Send. The over-read is
only reachable by calling the native binding method directly, bypassing the wrapper:

// Direct-binding path only (bypasses the public index.js validation):
const raw = require('bindings')('raw');
const wrap = new raw.SocketWrap(raw.Protocol.None, raw.AddressFamily.IPv4);
const buf = Buffer.alloc(16);
wrap.send(buf, 0, 4096, sockaddrBuf, () => {}); // offset+length exceed 16-byte buffer -> OOB read

Reachability

Raw-binding reachable only. The public Socket.prototype.send bounds-checks
length + offset > buffer.length and throws, so the disclosure cannot be triggered via the
supported API — only via a direct SocketWrap.send(...) call on the internal binding.

Suggested fix: before computing data, validate the range in native code too, e.g.
if (offset > bufLen || length > bufLen - offset) { throw RangeError; } where
bufLen = node::Buffer::Length(buffer), mirroring the checks in CreateChecksum/SetOption (so
the binding is memory-safe independent of the JS wrapper).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions