Introduction
In a backend Express.js application, we discovered a high-severity rate limiting bypass in backend/package-lock.json affecting the express-rate-limit dependency version 8.2.1. This vulnerability, tracked as CVE-2026-30827, allowed attackers on dual-stack networks to completely bypass per-client rate limits by exploiting how the library handled IPv4-mapped IPv6 addresses. The flawed subnet masking logic in the underlying ip-address library (version 10.0.1) failed to correctly identify unique IPv4 clients when they connected through IPv6, treating multiple distinct clients as a single entity—or worse, failing to track them at all.
For applications relying on express-rate-limit to prevent brute force attacks, credential stuffing, or API abuse, this vulnerability created a critical security gap. An attacker could send unlimited requests from multiple IPv4 addresses, all appearing to the rate limiter as coming from the same (or untrackable) source, effectively nullifying the protection.
The Vulnerability Explained
The root cause lies in how express-rate-limit version 8.2.1 (and its dependency ip-address 10.0.1) processes client IP addresses on servers with dual-stack network configurations. Here's what happened:
When an IPv4 client connects to a dual-stack server (one supporting both IPv4 and IPv6), the connection may be represented as an IPv4-mapped IPv6 address. For example, the IPv4 address 192.0.2.1 becomes ::ffff:192.0.2.1 in IPv6 notation. This is standard behavior in dual-stack networking.
The vulnerable code in express-rate-limit 8.2.1 used ip-address version 10.0.1 to parse and normalize these addresses for rate limiting. However, the subnet masking logic in ip-address 10.0.1 incorrectly handled IPv4-mapped IPv6 addresses, leading to one of two failure modes:
- Client conflation: Multiple distinct IPv4 clients were treated as the same client, causing legitimate users to share rate limit quotas
- Client invisibility: IPv4-mapped addresses weren't properly tracked, allowing unlimited requests
Let's look at the vulnerable dependency specification from backend/package.json:
"express-rate-limit": "^8.2.1"
And the corresponding lock file entry in backend/package-lock.json:
"node_modules/express-rate-limit": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
"license": "MIT",
"dependencies": {
"ip-address": "10.0.1"
}
}
The problem is in the pinned dependency: ip-address: 10.0.1. This version contained the flawed subnet masking implementation.
Real-World Attack Scenario
Consider a backend API endpoint protected by express-rate-limit with a 100 requests/hour limit per client:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 100, // 100 requests per hour
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
An attacker on a dual-stack network could:
- Send 100 requests from IP
192.0.2.1(appears as::ffff:192.0.2.1) - Send 100 more requests from IP
192.0.2.2(appears as::ffff:192.0.2.2) - Send 100 more requests from IP
192.0.2.3(appears as::ffff:192.0.2.3)
Due to the incorrect subnet masking in ip-address 10.0.1, express-rate-limit might:
- Treat all three IPs as the same client (reaching the limit after just 100 requests, blocking legitimate users)
- Fail to track them properly (allowing all 300+ requests through, enabling DoS)
Both outcomes are security failures. The attacker could continue this pattern across an entire subnet, launching a denial of service attack against the API by either exhausting rate limits for legitimate users or flooding the server with unlimited requests.
The Fix
The fix involves upgrading two packages to versions with corrected IPv6 subnet masking logic:
- express-rate-limit: 8.2.1 → 8.2.2
- ip-address: 10.0.1 → 10.1.0 (transitive dependency)
Before (Vulnerable):
"express-rate-limit": "^8.2.1"
"node_modules/express-rate-limit": {
"version": "8.2.1",
"dependencies": {
"ip-address": "10.0.1"
}
}
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="
}
After (Fixed):
"express-rate-limit": "^8.2.2"
"node_modules/express-rate-limit": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.2.tgz",
"integrity": "sha512-Ybv7bqtOgA914MLwaHWVFXMpMYeR1MQu/D+z2MaLYteqBsTIp9sY3AU7mGNLMJv8eLg8uQMpE20I+L2Lv49nSg==",
"dependencies": {
"ip-address": "10.1.0"
}
}
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="
}
How This Fix Solves the Problem
The ip-address library version 10.1.0 implements correct subnet masking for IPv4-mapped IPv6 addresses. Specifically:
- IPv4-mapped addresses like
::ffff:192.0.2.1are now properly normalized and compared - The subnet masking algorithm correctly identifies unique /32 IPv4 addresses within the IPv4-mapped IPv6 space
- Each distinct IPv4 client (192.0.2.1, 192.0.2.2, etc.) is now tracked separately by express-rate-limit
- Rate limit quotas are enforced per actual client, not per incorrectly-masked subnet
This change was necessary in both files because:
backend/package.json: Updates the direct dependency version constraint to^8.2.2, ensuring future installs get the fixed versionbackend/package-lock.json: Locks both express-rate-limit to 8.2.2 AND ip-address to 10.1.0, guaranteeing the correct transitive dependency is installed
The fix maintains full backward compatibility—all legitimate client requests continue to work exactly as before. The only change is that rate limiting now works correctly on dual-stack networks, properly identifying and tracking individual IPv4 clients.
Prevention & Best Practices
1. Keep Rate Limiting Libraries Updated
Rate limiting is a critical security control. Treat rate limiting dependencies with the same urgency as authentication libraries:
# Regularly audit and update security-sensitive packages
npm audit
npm update express-rate-limit
2. Test Rate Limiting on Dual-Stack Networks
If your infrastructure supports both IPv4 and IPv6, explicitly test rate limiting behavior:
// Test with IPv4-mapped IPv6 addresses
const testIPs = [
'::ffff:192.0.2.1',
'::ffff:192.0.2.2',
'::ffff:192.0.2.3'
];
// Verify each IP is tracked separately
3. Monitor Rate Limiter Effectiveness
Implement monitoring to detect rate limiter bypass attempts:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 100,
handler: (req, res) => {
// Log rate limit violations for security monitoring
console.warn(`Rate limit exceeded for IP: ${req.ip}`);
res.status(429).json({ error: 'Too many requests' });
}
});
4. Use Dependency Scanning in CI/CD
Integrate tools like Trivy, Snyk, or npm audit into your pipeline:
# .github/workflows/security.yml
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
5. Consider Defense in Depth
Don't rely solely on application-level rate limiting:
- Network-level rate limiting: Use load balancers (AWS ALB, Nginx) for additional protection
- WAF rules: Cloud WAFs can provide IP-based rate limiting before requests reach your app
- Connection limits: Configure reverse proxies to limit connections per IP
6. Follow OWASP Guidelines
Refer to OWASP API Security Top 10 - API4:2023 Unrestricted Resource Consumption for comprehensive rate limiting strategies.
Key Takeaways
- IPv4-mapped IPv6 addresses (
::ffff:x.x.x.x) can bypass rate limiting when libraries incorrectly handle subnet masking on dual-stack servers - express-rate-limit 8.2.1 with ip-address 10.0.1 contained flawed subnet masking logic that failed to distinguish between unique IPv4 clients connecting through IPv6
- Upgrading to express-rate-limit 8.2.2 (with ip-address 10.1.0) fixes the subnet masking algorithm, ensuring each IPv4 client is correctly identified and rate-limited
- Dual-stack network configurations are increasingly common in cloud environments, making this vulnerability likely to be exploitable in production systems
- Rate limiting bypass vulnerabilities can enable denial of service attacks, brute force attempts, and API abuse that directly impact application availability and security
How Orbis AppSec Detected This
- Source: HTTP client connections on dual-stack network interfaces, where IPv4 addresses are represented as IPv4-mapped IPv6 addresses (
::ffff:x.x.x.x) - Sink:
ip-addresslibrary version 10.0.1 subnet masking logic called byexpress-rate-limit8.2.1 inbackend/package-lock.json - Missing control: Correct IPv6 subnet masking algorithm for IPv4-mapped addresses, allowing client identification bypass
- CWE: CWE-670 (Always-Incorrect Control Flow Implementation)
- Fix: Upgraded express-rate-limit to 8.2.2 and ip-address to 10.1.0, implementing correct subnet masking for IPv4-mapped IPv6 addresses
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-30827 demonstrates how subtle networking issues can create serious security vulnerabilities. The incorrect handling of IPv4-mapped IPv6 addresses in express-rate-limit 8.2.1 could have allowed attackers to bypass rate limits entirely, enabling denial of service attacks and API abuse. By upgrading to version 8.2.2 with the fixed ip-address 10.1.0 dependency, applications can ensure that rate limiting works correctly on modern dual-stack networks.
This vulnerability highlights the importance of keeping security-critical dependencies updated, testing rate limiting behavior across different network configurations, and implementing defense-in-depth strategies. Rate limiting is often the first line of defense against automated attacks—when it fails, the entire application becomes vulnerable.