From e30ec2737e4a063140293c1705dd84e72b3e3812 Mon Sep 17 00:00:00 2001 From: some random guy Date: Sun, 2 Aug 2020 17:40:03 +0200 Subject: [PATCH] normalize IP addresses Currently jschan takes the IP address as a string from the `X-Real-Ip` header, which based on the frontend proxy configuration, OS settings, etc. can take various forms: IPv4 addresses can be given in normal IPv4 dotted notation (e.g. `1.2.3.4`) or as an IPv4-mapped IPv6 address (e.g. `::ffff:1.2.3.4`). The problem is, that in the latter case, node's `isIP` will report 6, so the code will try to split it along colons, breaking hrange and qrange. With IPv6 addresses, it's possible to elide runs of zeroes, so `::1` and `0:0:0:0:0:0:0:1` (and also `0000:0000:0000:0000:0000:0000:0000:0001`) represents the same address. Since it's pretty easy to get a /64 IPv6 block, a spammer can abuse it, by spamming from `a:b:c:d::1` (`qrange=a:b:c:d`, `hrange=a:b:c`), then from `a:b:c:d::1:1` (`qrange=a:b:c:d:`, `hrange=a:b:c`), `a:b:c:d::1:1:1` (`qrange=a:b:c:d::1`, `hrange=a:b:c:d`) and `a:b:c:d:1:1:1:1` (`qrange=a:b:c:d:1:1`, `hrange=a:b:c:d`). He practically got two hranges and qrange is pretty much pointless for IPv6 addresses. This change uses the `ip6addr` package to parse IP addresses and convert it to some canonical form. This means: * IPv4 and IPv4-mapped IPv6 addresses are converted to normal IPv4 notation. * Zero are not elided in IPv6 (so you'll never see `::`). * IPv6 addresses are not zero padded (so `..:1` instead of `..:0001`). * Even though it's not documented, it seems like `ip6addr` always generates lower-case letters. This will unfortunately mean that some IP hashes may change after the update. Normal IPv4 hashes will most probably remain the same though. --- helpers/processip.js | 22 ++++++++++++++-------- package-lock.json | 20 ++++++++++++-------- package.json | 1 + 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/helpers/processip.js b/helpers/processip.js index 97194739..e95efd9d 100644 --- a/helpers/processip.js +++ b/helpers/processip.js @@ -1,25 +1,31 @@ 'use strict'; const { ipHashPermLevel } = require(__dirname+'/../configs/main.js') - , { isIP } = require('net') + , { parse } = require('ip6addr') , hashIp = require(__dirname+'/haship.js'); module.exports = (req, res, next) => { const ip = req.headers['x-real-ip'] || req.connection.remoteAddress; //need to consider forwarded-for, etc here and in nginx - const ipVersion = isIP(ip); - if (ipVersion) { - const delimiter = ipVersion === 4 ? '.' : ':'; - let split = ip.split(delimiter); + try { + const ipParsed = parse(ip); + const ipStr = ipParsed.toString({ + format: ipParsed.kind() === 'ipv4' ? 'v4' : 'v6', + zeroElide: false, + zeroPad: false, + }); + const delimiter = ipParsed.kind() === 'ipv4' ? '.' : ':'; + let split = ipStr.split(delimiter); const qrange = split.slice(0,Math.floor(split.length*0.75)).join(delimiter); const hrange = split.slice(0,Math.floor(split.length*0.5)).join(delimiter); res.locals.ip = { - raw: ipHashPermLevel === -1 ? hashIp(ip) : ip, - single: hashIp(ip), + raw: ipHashPermLevel === -1 ? hashIp(ipStr) : ipStr, + single: hashIp(ipStr), qrange: hashIp(qrange), hrange: hashIp(hrange), } next(); - } else { + } catch(e) { + console.error('Ip parse failed', e); return res.status(400).render('message', { 'title': 'Bad request', 'message': 'Malformed IP' //should never get here diff --git a/package-lock.json b/package-lock.json index 2c583010..11c77546 100644 --- a/package-lock.json +++ b/package-lock.json @@ -767,8 +767,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assign-symbols": { "version": "1.0.0", @@ -2675,8 +2674,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "optional": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fancy-log": { "version": "1.3.3", @@ -3994,6 +3992,15 @@ "ipaddr.js": "^1.8.1" } }, + "ip6addr": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ip6addr/-/ip6addr-0.2.3.tgz", + "integrity": "sha512-qA9DXRAUW+lT47/i/4+Q3GHPwZjGt/atby1FH/THN6GVATA6+Pjp2nztH7k6iKeil7hzYnBwfSsxjthlJ8lJKw==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.4.0" + } + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", @@ -4259,8 +4266,7 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "optional": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", @@ -4292,7 +4298,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "optional": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -7909,7 +7914,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "optional": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", diff --git a/package.json b/package.json index edde4d46..0b1f10a2 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "gulp-uglify-es": "^2.0.0", "highlight.js": "^10.1.2", "ioredis": "^4.14.1", + "ip6addr": "^0.2.3", "mongodb": "^3.6.0", "node-fetch": "^2.6.0", "path": "^0.12.7",