Skip to content

[SECURITY] JavaScript Signed 32-Bit Integer Overflow in IP Parsing Bypasses SSRF Protection for 192.168.x.x and 172.16.x.x #3118

Description

@basantnema31

Description

The application implements SSRF protection in src/lib/ssrf-protection.ts by checking resolved IP addresses against a list of private IP ranges (PRIVATE_RANGES).
To perform this check, the ipToNumber function parses the IP address string and converts it to a number using bitwise shifting:

function ipToNumber(ip: string): number {
  const parts = ip.split(".").map(Number);
  return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
}

In JavaScript, all bitwise operations are performed on 32-bit signed integers. When parts[0] is 172 (or greater), shifting it left by 24 bits (parts[0] << 24) overflows the signed 32-bit limit and results in a negative integer (e.g. 192.168.1.1 becomes -1062731519).

The range check is performed as follows:

PRIVATE_RANGES.some(({ start, end }) => num >= start && num <= end)

However, the boundary values in PRIVATE_RANGES are defined as hex literals:

const PRIVATE_RANGES = [
  { start: 0x0a000000, end: 0x0affffff }, // 10.0.0.0/8
  { start: 0xac100000, end: 0xac1fffff }, // 172.16.0.0/12
  { start: 0xc0a80000, end: 0xc0a8ffff }, // 192.168.0.0/16
  { start: 0x7f000000, end: 0x7fffffff }, // 127.0.0.0/8
  { start: 0xa9fe0000, end: 0xa9feffff }, // 169.254.0.0/16
];

In JavaScript, these hex literals are represented as positive numbers (e.g., 0xc0a80000 is 3232235520). Comparing the negative signed integer representation of 192.168.x.x or 172.16.x.x (which is negative) to these positive bounds fails the check completely (num >= start is false).

Impact

As a result of this overflow, the SSRF filter evaluates 192.168.x.x and 172.16.x.x IP addresses as safe, permitting custom webhooks or other outbound requests to be dispatched to these internal network segments. Attackers can exploit this to access internal network services and resources.

Affected Files

  • ssrf-protection.ts

Steps to Reproduce

Run the following JS code:

const PRIVATE_RANGES = [
  { start: 0x0a000000, end: 0x0affffff },
  { start: 0xac100000, end: 0xac1fffff },
  { start: 0xc0a80000, end: 0xc0a8ffff },
];
function ipToNumber(ip) {
  const parts = ip.split('.').map(Number);
  return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
}
function isPrivateIP(ip) {
  const num = ipToNumber(ip);
  return PRIVATE_RANGES.some(({ start, end }) => num >= start && num <= end);
}
console.log(isPrivateIP('192.168.1.1')); // Output: false

Suggested Solution

Use unsigned shifts (>>>) or simple arithmetic to construct the IP number, ensuring it is a positive integer:

function ipToNumber(ip: string): number {
  const parts = ip.split(".").map(Number);
  return (parts[0] * 16777216) + (parts[1] * 65536) + (parts[2] * 256) + parts[3];
}

Labels: gssoc, quality:exceptional, level:critical

/assign

Metadata

Metadata

Labels

gssoc:assignedGSSoC: Issue assigned to a contributor

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions