From d68e5adf0bff81691a1a1e966f3c84fc487bf0ce Mon Sep 17 00:00:00 2001 From: Kagami Hiiragi Date: Fri, 27 Feb 2015 15:54:05 +0300 Subject: [PATCH] Filter out private IPs in addr messages Fixes #10 --- lib/_util.js | 81 ++++++++++++++++++++++++++++++++++++++++++++++ lib/messages.js | 62 +++++++++++++++++++++++++++++++++-- lib/structs.js | 86 +++---------------------------------------------- test.js | 26 ++++++++++++++- 4 files changed, 171 insertions(+), 84 deletions(-) diff --git a/lib/_util.js b/lib/_util.js index e6571e2..86bcab7 100644 --- a/lib/_util.js +++ b/lib/_util.js @@ -94,3 +94,84 @@ exports.popkey = function(obj, key) { delete obj[key]; return value; }; + +// See https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses +var IPv4_MAPPING = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255]); +exports.IPv4_MAPPING = IPv4_MAPPING; + +// Very simple inet_pton(3) equivalent. +exports.inet_pton = function(str) { + var buf = new Buffer(16); + buf.fill(0); + // IPv4-mapped IPv6. + if (str.slice(0, 7) === "::ffff:") { + str = str.slice(7); + } + // IPv4. + if (str.indexOf(":") === -1) { + IPv4_MAPPING.copy(buf); + var octets = str.split(/\./g).map(function(o) { + assert(/^\d+$/.test(o), "Bad octet"); + return parseInt(o, 10); + }); + // Support short form from inet_aton(3) man page. + if (octets.length === 1) { + buf.writeUInt32BE(octets[0], 12); + } else { + // Check against 1000.bad.addr + octets.forEach(function(octet) { + assert(octet >= 0, "Bad IPv4 address"); + assert(octet <= 255, "Bad IPv4 address"); + }); + if (octets.length === 2) { + buf[12] = octets[0]; + buf[15] = octets[1]; + } else if (octets.length === 3) { + buf[12] = octets[0]; + buf[13] = octets[1]; + buf[15] = octets[2]; + } else if (octets.length === 4) { + buf[12] = octets[0]; + buf[13] = octets[1]; + buf[14] = octets[2]; + buf[15] = octets[3]; + } else { + throw new Error("Bad IPv4 address"); + } + } + // IPv6. + } else { + var dgroups = str.split(/::/g); + // Check against 1::1::1 + assert(dgroups.length <= 2, "Bad IPv6 address"); + var groups = []; + var i; + if (dgroups[0]) { + groups.push.apply(groups, dgroups[0].split(/:/g)); + } + if (dgroups.length === 2) { + if (dgroups[1]) { + var splitted = dgroups[1].split(/:/g); + var fill = 8 - (groups.length + splitted.length); + // Check against 1:1:1:1::1:1:1:1 + assert(fill > 0, "Bad IPv6 address"); + for (i = 0; i < fill; i++) { + groups.push(0); + } + groups.push.apply(groups, splitted); + } else { + // Check against 1:1:1:1:1:1:1:1:: + assert(groups.length <= 7, "Bad IPv6 address"); + } + } else { + // Check against 1:1:1 + assert(groups.length === 8, "Bad IPv6 address"); + } + for (i = 0; i < Math.min(groups.length, 8); i++) { + // Check against parseInt("127.0.0.1", 16) -> 295 + assert(/^[0-9a-f]+$/.test(groups[i]), "Bad group"); + buf.writeUInt16BE(parseInt(groups[i], 16), i * 2); + } + } + return buf; +}; diff --git a/lib/messages.js b/lib/messages.js index 729a2ff..7dc15f8 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -33,6 +33,7 @@ "use strict"; var objectAssign = Object.assign || require("object-assign"); +var bufferEqual = require("buffer-equal"); var assert = require("./_util").assert; var structs = require("./structs"); var bmcrypto = require("./crypto"); @@ -41,6 +42,8 @@ var util = require("./_util"); var message = structs.message; var ServicesBitfield = structs.ServicesBitfield; +var IPv4_MAPPING = util.IPv4_MAPPING; +var inet_pton = util.inet_pton; /** * Try to get command of the given encoded message. @@ -226,6 +229,54 @@ var version = exports.version = { }, }; +var IPv6_LOOPBACK = new Buffer( + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] +); + +// Check whether given encoded IPv6 or IPv4-mapped IPv6 is in private +// network range. See +// for details. +// TODO(Kagami): Do we also need to filter multicasts and other reserved +// ranges? +function isPrivateIp(buf) { + // IPv4. + if (bufferEqual(buf.slice(0, 12), IPv4_MAPPING)) { + buf = buf.slice(12); + if (buf[0] === 127) { + return true; + } else if (buf[0] === 10) { + return true; + } else if (buf[0] === 192 && buf[1] === 168) { + return true; + // XXX(Kagami): ignore:start and ignore:end doesn't ignore this for + // some reason. Probably related: + // . + } else if (buf[0] === 172 && (buf[1] & 0xf0) === 0x10) {//jshint ignore:line + return true; + } else if (buf[0] === 169 && buf[1] === 254) { + return true; + } else { + return false; + } + // IPv6. + } else { + if (bufferEqual(buf, IPv6_LOOPBACK)) { + return true; + } else if (buf[0] === 0xfe && (buf[1] & 0xc0) === 0x80) {//jshint ignore:line + return true; + } else if ((buf[0] & 0xfe) === 0xfc) { // jshint ignore:line + return true; + } else { + return false; + } + } +} + +// Helper to make it easier to filter out private IPs. +function checkAddrOpts(opts) { + return !isPrivateIp(inet_pton(opts.host)); +} + /** * `addr` message. Provide information on known nodes of the network. * @see {@link https://bitmessage.org/wiki/Protocol_specification#addr} @@ -260,13 +311,19 @@ var addr = exports.addr = { decodePayload: function(buf) { var decoded = structs.var_int.decode(buf); var listLength = decoded.value; + // NOTE(Kagami): Check length before filtering private IPs because + // we shouldn't even receive them. assert(listLength <= 1000, "Too many address entires"); var length = decoded.length + listLength * 38; assert(buf.length >= length, "Buffer is too small"); var rest = decoded.rest; - var addrs = new Array(listLength); + var addrs = []; + var addrBuf; for (var i = 0; i < listLength; i++) { - addrs[i] = structs.net_addr.decode(rest.slice(i*38, (i+1)*38)); + addrBuf = rest.slice(i*38, (i+1)*38); + if (!isPrivateIp(addrBuf.slice(20, 36))) { + addrs.push(structs.net_addr.decode(addrBuf)); + } } return { addrs: addrs, @@ -291,6 +348,7 @@ var addr = exports.addr = { * The same as [encode]{@link module:bitmessage/messages.addr.encode}. */ encodePayload: function(addrs) { + addrs = addrs.filter(checkAddrOpts); assert(addrs.length <= 1000, "Too many address entires"); var addrBufs = addrs.map(structs.net_addr.encode); var bufs = [structs.var_int.encode(addrs.length)].concat(addrBufs); diff --git a/lib/structs.js b/lib/structs.js index 34d2a7e..4ff3a7c 100644 --- a/lib/structs.js +++ b/lib/structs.js @@ -27,11 +27,14 @@ var objectAssign = Object.assign || require("object-assign"); var bufferEqual = require("buffer-equal"); -var assert = require("./_util").assert; var bmcrypto = require("./crypto"); var POW = require("./pow"); var util = require("./_util"); +var assert = util.assert; +var IPv4_MAPPING = util.IPv4_MAPPING; +var inet_pton = util.inet_pton; + function isAscii(str) { for (var i = 0; i < str.length; i++) { if (str.charCodeAt(i) > 127) { @@ -678,9 +681,6 @@ exports.var_int_list = { }, }; -// See https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses -var IPv4_MAPPING = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255]); - // Very simple inet_ntop(3) equivalent. function inet_ntop(buf) { assert(buf.length === 16, "Bad buffer size"); @@ -689,6 +689,7 @@ function inet_ntop(buf) { return Array.prototype.join.call(buf.slice(12), "."); // IPv6. } else { + // TODO(Kagami): Join empty groups to make address looks nicer. var groups = []; for (var i = 0; i < 8; i++) { groups.push(buf.readUInt16BE(i * 2, true).toString(16)); @@ -697,83 +698,6 @@ function inet_ntop(buf) { } } -// Very simple inet_pton(3) equivalent. -function inet_pton(str) { - var buf = new Buffer(16); - buf.fill(0); - // IPv4-mapped IPv6. - if (str.slice(0, 7) === "::ffff:") { - str = str.slice(7); - } - // IPv4. - if (str.indexOf(":") === -1) { - IPv4_MAPPING.copy(buf); - var octets = str.split(/\./g).map(function(o) { - assert(/^\d+$/.test(o), "Bad octet"); - return parseInt(o, 10); - }); - // Support short form from inet_aton(3) man page. - if (octets.length === 1) { - buf.writeUInt32BE(octets[0], 12); - } else { - // Check against 1000.bad.addr - octets.forEach(function(octet) { - assert(octet >= 0, "Bad IPv4 address"); - assert(octet <= 255, "Bad IPv4 address"); - }); - if (octets.length === 2) { - buf[12] = octets[0]; - buf[15] = octets[1]; - } else if (octets.length === 3) { - buf[12] = octets[0]; - buf[13] = octets[1]; - buf[15] = octets[2]; - } else if (octets.length === 4) { - buf[12] = octets[0]; - buf[13] = octets[1]; - buf[14] = octets[2]; - buf[15] = octets[3]; - } else { - throw new Error("Bad IPv4 address"); - } - } - // IPv6. - } else { - var dgroups = str.split(/::/g); - // Check against 1::1::1 - assert(dgroups.length <= 2, "Bad IPv6 address"); - var groups = []; - var i; - if (dgroups[0]) { - groups.push.apply(groups, dgroups[0].split(/:/g)); - } - if (dgroups.length === 2) { - if (dgroups[1]) { - var splitted = dgroups[1].split(/:/g); - var fill = 8 - (groups.length + splitted.length); - // Check against 1:1:1:1::1:1:1:1 - assert(fill > 0, "Bad IPv6 address"); - for (i = 0; i < fill; i++) { - groups.push(0); - } - groups.push.apply(groups, splitted); - } else { - // Check against 1:1:1:1:1:1:1:1:: - assert(groups.length <= 7, "Bad IPv6 address"); - } - } else { - // Check against 1:1:1 - assert(groups.length === 8, "Bad IPv6 address"); - } - for (i = 0; i < Math.min(groups.length, 8); i++) { - // Check against parseInt("127.0.0.1", 16) -> 295 - assert(/^[0-9a-f]+$/.test(groups[i]), "Bad group"); - buf.writeUInt16BE(parseInt(groups[i], 16), i * 2); - } - } - return buf; -} - /** * Network address. * @see {@link https://bitmessage.org/wiki/Protocol_specification#Network_address} diff --git a/test.js b/test.js index 09fa690..73ac5bf 100644 --- a/test.js +++ b/test.js @@ -556,9 +556,33 @@ describe("Message types", function() { }); it("shouldn't encode/decode more than 1000 entires", function() { - expect(addr.encode.bind(null, Array(2000))).to.throw(/too many/i); + var addrs = new Array(1001); + var ip = {host: "1.2.3.4"}; + for (var i = 0; i < 1001; i++) { + addrs[i] = ip; + } + expect(addr.encode.bind(null, addrs)).to.throw(/too many/i); expect(addr.decodePayload.bind(null, var_int.encode(2000))).to.throw(/too many/i); }); + + it("should filter out private IP ranges", function() { + expect(addr.encodePayload([ + {host: "127.0.0.1", port: 1}, + {host: "127.5.3.1", port: 2}, + {host: "1.2.3.4", port: 3, time: new Date(1425034238202)}, + {host: "192.168.15.20", port: 4}, + {host: "10.10.10.10", port: 5}, + {host: "172.17.42.1", port: 6}, + {host: "::1", port: 7}, + {host: "fe80::1:2:3", port: 8}, + {host: "fc00::3:2:1", port: 9}, + ]).toString("hex")).to.equal("010000000054f04bfe00000001000000000000000100000000000000000000ffff010203040003"); + + var res = addr.decodePayload(Buffer("090000000054f04b9b00000001000000000000000100000000000000000000ffff7f00000100010000000054f04b9b00000001000000000000000100000000000000000000ffff7f05030100020000000054f04b9b00000001000000000000000100000000000000000000ffff0102030400030000000054f04b9b00000001000000000000000100000000000000000000ffffc0a80f1400040000000054f04b9b00000001000000000000000100000000000000000000ffff0a0a0a0a00050000000054f04b9b00000001000000000000000100000000000000000000ffffac112a0100060000000054f04b9b0000000100000000000000010000000000000000000000000000000100070000000054f04b9b000000010000000000000001fe80000000000000000000010002000300080000000054f04b9b000000010000000000000001fc0000000000000000000003000200010009", "hex")); + expect(res.addrs).to.have.length(1); + expect(res.addrs[0].host).to.equal("1.2.3.4"); + expect(res.addrs[0].port).to.equal(3); + }); }); describe("inv", function() {