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🅱️c:d::1` (`qrange=a🅱️c:d`,
`hrange=a🅱️c`), then from `a🅱️c:d::1:1` (`qrange=a🅱️c:d:`, `hrange=a🅱️c`),
`a🅱️c:d::1:1:1` (`qrange=a🅱️c:d::1`, `hrange=a🅱️c:d`) and
`a🅱️c:d:1:1:1:1` (`qrange=a🅱️c:d:1:1`, `hrange=a🅱️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.
merge-requests/208/head
some random guy 4 years ago
parent ba5d35813a
commit e30ec2737e
  1. 22
      helpers/processip.js
  2. 20
      package-lock.json
  3. 1
      package.json

@ -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

20
package-lock.json generated

@ -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",

@ -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",

Loading…
Cancel
Save