Ghost in the Hostname Buffer — JavaScript Bug Hunt
Modelled on GHOST (CVE-2015-0235, disclosed by Qualys in January 2015): glibc's nsshostnamedigitsdots, used by gethostbyname, answers numeric hostnames like…
- Language: JavaScript
- Layer: Backend
- Difficulty: Hard
- Concepts: Security, Overflow, Networking
- Modelled on: glibc · CVE-2015-0235
- Visible tests: a numeric host fits in a roomy buffer; the size covers both pointers
- Reward: 50 XP for a complete fix
Briefing
Modelled on GHOST (CVE-2015-0235, disclosed by Qualys in January 2015): glibc's __nss_hostname_digits_dots, used by gethostbyname, answers numeric hostnames like "10.0.0.1" without a DNS lookup and lays the result out in a caller-supplied buffer. Its size calculation left out one pointer (sizeof(char *)), so a crafted numeric hostname could write a few bytes past the end of the buffer.
digitsdots.js reconstructs that path over a simulated byte buffer (membuf.js) that, like C, records an out-of-bounds write instead of stopping it.
Fix requiredSize and resolve so the buffer is sized for everything written into it and nothing is ever written past its end.
Bug report
BUG-GHOST · Priority: Critical (heap overflow) · Reported by: security
Layout written for a numeric host (see layout.js): [ address: ADDR_SIZE | h_addr_list pointer: PTR_SIZE | h_aliases pointer: PTR_SIZE | name bytes | NUL ]
requiredSize(name) = ADDR_SIZE + 2 * PTR_SIZE + name.length + 1 requiredSize("10.0.0.1") -> 29
resolve(name, buf)
- a name that is not only digits and dots (or is empty) -> { ok: false, error: "NOT_NUMERIC" }
- buf.size < requiredSize(name) -> { ok: false, error: "ERANGE" } and NOTHING is written
- otherwise the layout is written and { ok: true, used: requiredSize(name) } returned
- buf.overflowed must never become true
Observed: a buffer 8 bytes smaller than needed passes the size check and the name is written past its end.
Logs
[resolver] gethostbyname("0000...0000") bufsize=1024 need=1032
[membuf] write at offset 1024 beyond size 1024The code as shipped
src/net/digitsdots.js (editable)
var layout = require("./layout");
function isNumericHost(name) {
return name.length > 0 && /^[0-9.]+$/.test(name);
}
function zeros(n) {
var out = [];
for (var i = 0; i < n; i++) out.push(0);
return out;
}
exports.requiredSize = function (name) {
return layout.ADDR_SIZE + layout.PTR_SIZE + name.length + 1;
};
exports.resolve = function (name, buf) {
if (!isNumericHost(name)) return { ok: false, error: "NOT_NUMERIC" };
if (buf.size < exports.requiredSize(name)) return { ok: false, error: "ERANGE" };
var addr = [0, 0, 0, 0];
var parts = name.split(".");
for (var i = 0; i < 4 && i < parts.length; i++) addr[i] = Number(parts[i]) & 255;
var at = buf.write(0, addr);
at = buf.write(at, zeros(layout.PTR_SIZE));
at = buf.write(at, zeros(layout.PTR_SIZE));
var chars = [];
for (var j = 0; j < name.length; j++) chars.push(name.charCodeAt(j));
chars.push(0);
at = buf.write(at, chars);
return { ok: true, used: at };
};
Read-only context: src/net/layout.js, src/net/membuf.js.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.