New Issue Checklist
Issue Description
Config.validateIps strips the CIDR mask before validating, so the mask itself is never checked:
static validateIps(field, masterKeyIps) {
for (let ip of masterKeyIps) {
if (ip.includes('/')) { ip = ip.split('/')[0]; }
if (!net.isIP(ip)) { throw `... contains an invalid IP address "${ip}".`; }
}
}
getBlockList then passes it through Number(mask) unchecked. Four consequences, all measured
against checkIp:
- An out-of-range or fractional mask starts the server and fails on first use.
127.0.0.1/999
and 10.0.0.0/8.9 pass boot validation, and BlockList.addSubnet then throws
The value of "prefix" is out of range on the first request that presents the key, which the
client sees as a 500. The option is unusable but nothing says so until it is exercised.
Number() silently accepts spellings that are not integers. 127.0.0.0/32.0,
127.0.0.0/3.2e1, 127.0.0.0/0x20, 127.0.0.0/0b100000, 127.0.0.0/ 32 and 127.0.0.0/32
all become a working /32.
- An empty mask silently changes the meaning of the entry.
127.0.0.1/ splits to an empty
mask, !mask reads that as absent, and the entry is added as a bare address.
- Anything after a second slash is discarded.
127.0.0.1/32/ignored is destructured to its
first two parts and the rest is dropped.
These matter more than an ordinary parsing nit because the option is an authorization boundary: a
typo either means something other than what was written, or waits until the first master-key request
to fail.
Steps to reproduce
const { checkIp } = require('parse-server/lib/middlewares');
const store = () => new Map();
checkIp('127.0.0.1', ['127.0.0.0/32.0'], store()); // true, treated as /32
checkIp('127.0.0.1', ['127.0.0.0/0x20'], store()); // true, treated as /32
checkIp('127.0.0.1', ['127.0.0.1/'], store()); // true, treated as a bare address
checkIp('127.0.0.1', ['127.0.0.1/32/ignored'], store()); // true, tail discarded
checkIp('127.0.0.1', ['127.0.0.1/999'], store()); // throws, out of range
Each of those entries also passes Config.validateIps, so a server configured with any of them
starts normally.
Actual Outcome
All five entries are accepted at boot. Four are silently reinterpreted, and the fifth throws on the
first request that presents the master key, producing a 500.
Expected Outcome
validateIps should validate the mask alongside the address and refuse the option at boot: an
integer, within 0-32 for IPv4 and 0-128 for IPv6, with no trailing content after the prefix.
Failing at startup names the offending entry while an operator is still looking at the
configuration.
Related, and possibly worth folding into the same fix
The option's help text says:
IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs
to be considered separately.
That is true of the five special-cased allow-all literals, which getBlockList scopes by family,
and of genuine addresses. It is not true of IPv4-mapped IPv6 addresses, which BlockList
unifies: checkIp('::ffff:127.0.0.1', ['127.0.0.1']) is true, while
checkIp('::1', ['0.0.0.0/0']) is false.
This is worth a sentence in the help text because it decides whether an allowlist works at all on a
dual-stack listener, where every IPv4 client arrives as ::ffff:<address>. An operator reading the
current text would reasonably conclude they must add both forms of every address, and an
implementer reading it would conclude the wrong thing about 0.0.0.0/0. This is adjacent to #8872,
which covered disabling the filter and was resolved as a docs change.
Environment
Server
- Parse Server version:
9.10.1-alpha.6 (commit ca75b1fe)
- Operating system:
macOS 26.5.2
- Local or remote host:
local
Database
- System (MongoDB or Postgres):
MongoDB
- Database version:
7.0.25
- Local or remote host:
local
Client
- SDK (iOS, Android, JavaScript, PHP, Unity, etc):
none, checkIp called directly
- SDK version:
n/a
Logs
RangeError [ERR_OUT_OF_RANGE]: The value of "prefix" is out of range. It must be >= 0 && <= 32. Received 999
Found while building a reimplementation and comparing behaviour against a server built at
ca75b1fe. Every result above came from calling checkIp directly rather than from reading the
source.
New Issue Checklist
Issue Description
Config.validateIpsstrips the CIDR mask before validating, so the mask itself is never checked:getBlockListthen passes it throughNumber(mask)unchecked. Four consequences, all measuredagainst
checkIp:127.0.0.1/999and
10.0.0.0/8.9pass boot validation, andBlockList.addSubnetthen throwsThe value of "prefix" is out of rangeon the first request that presents the key, which theclient sees as a 500. The option is unusable but nothing says so until it is exercised.
Number()silently accepts spellings that are not integers.127.0.0.0/32.0,127.0.0.0/3.2e1,127.0.0.0/0x20,127.0.0.0/0b100000,127.0.0.0/ 32and127.0.0.0/32all become a working
/32.127.0.0.1/splits to an emptymask,
!maskreads that as absent, and the entry is added as a bare address.127.0.0.1/32/ignoredis destructured to itsfirst two parts and the rest is dropped.
These matter more than an ordinary parsing nit because the option is an authorization boundary: a
typo either means something other than what was written, or waits until the first master-key request
to fail.
Steps to reproduce
Each of those entries also passes
Config.validateIps, so a server configured with any of themstarts normally.
Actual Outcome
All five entries are accepted at boot. Four are silently reinterpreted, and the fifth throws on the
first request that presents the master key, producing a 500.
Expected Outcome
validateIpsshould validate the mask alongside the address and refuse the option at boot: aninteger, within
0-32for IPv4 and0-128for IPv6, with no trailing content after the prefix.Failing at startup names the offending entry while an operator is still looking at the
configuration.
Related, and possibly worth folding into the same fix
The option's help text says:
That is true of the five special-cased allow-all literals, which
getBlockListscopes by family,and of genuine addresses. It is not true of IPv4-mapped IPv6 addresses, which
BlockListunifies:
checkIp('::ffff:127.0.0.1', ['127.0.0.1'])istrue, whilecheckIp('::1', ['0.0.0.0/0'])isfalse.This is worth a sentence in the help text because it decides whether an allowlist works at all on a
dual-stack listener, where every IPv4 client arrives as
::ffff:<address>. An operator reading thecurrent text would reasonably conclude they must add both forms of every address, and an
implementer reading it would conclude the wrong thing about
0.0.0.0/0. This is adjacent to #8872,which covered disabling the filter and was resolved as a docs change.
Environment
Server
9.10.1-alpha.6(commitca75b1fe)macOS 26.5.2localDatabase
MongoDB7.0.25localClient
none, checkIp called directlyn/aLogs
Found while building a reimplementation and comparing behaviour against a server built at
ca75b1fe. Every result above came from callingcheckIpdirectly rather than from reading thesource.