Incorrect Parsing of Numbers with Different Radices
Description
Incorrect Parsing of Numbers with Different Radices occurs when an application assumes base-10 numeric values but fails to handle inputs using alternative bases. Numbers prefixed with "0" are often treated as octal, while "0x" indicates hexadecimal, potentially causing unexpected behavior. If developers assume decimal-only inputs, the code could produce incorrect numbers when the inputs are parsed using a different base. A notable example involves IP addresses: "0127.0.0.1" in octal equals 87.0.0.1 in decimal. This weakness enables attackers to bypass access controls, manipulate SSRF protections, and exploit symbolic identifiers treated as numbers.
Risk
Radix parsing vulnerabilities have severe implications. Access control bypass through IP address manipulation. SSRF filter bypass using octal/hex addresses. Incorrect permission checks. Unexpected behavior in numeric comparisons. Configuration errors from typos. Security boundary bypass. Data integrity issues. High likelihood when validating IP addresses or other numeric identifiers.
Solution
Convert numerical strings to base-10 integers before conditional checks to prevent octal/hex processing during implementation phase. Check for base indicators (like "0x") and convert strings to appropriate bases while rejecting unsupported alternatives. Use anchored regular expressions (^ and $) when validating IP addresses to prevent base-prepended addresses from matching. Explicitly specify the radix when parsing numbers.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Attackers may access private resources using unexpected numerical bases. |
| Integrity | Scope: Integrity Attacker may use an unexpected numerical base to bypass or manipulate access control mechanisms. |
Example Code
Vulnerable Code
# Vulnerable: Python IP validation with radix issues
import subprocess
import re
# VULNERABLE: Octal parsing in IP validation
def vulnerable_validate_ip(ip_string):
"""Validates IP but returns original string with potential octal."""
parts = ip_string.split('.')
if len(parts) != 4:
return None
for part in parts:
try:
# VULNERABLE: int() handles octal (0-prefix) and hex (0x-prefix)
value = int(part, 0) # Base 0 = auto-detect
# Even with explicit base 10, leading zeros might be stripped
# but the original string is returned
if value < 0 or value > 255:
return None
except ValueError:
return None
# VULNERABLE: Returns original string, not normalized decimal
return ip_string
def vulnerable_ping(user_ip):
"""Pings user-provided IP address."""
validated_ip = vulnerable_validate_ip(user_ip)
if validated_ip:
# VULNERABLE: System interprets octal differently
# User input: "0127.0.0.1"
# Validation sees: 127.0.0.1 (after int conversion)
# ping sees: 87.0.0.1 (octal interpretation)
subprocess.run(['ping', '-c', '1', validated_ip])
# Attack: vulnerable_ping("0127.0.0.1")
# Validated as 127.0.0.1 but pings 87.0.0.1
# VULNERABLE: Regex without anchors
def vulnerable_validate_ip_regex(ip_string):
"""IP validation with unanchored regex."""
# VULNERABLE: No ^ and $ anchors
pattern = r'(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})'
match = re.search(pattern, ip_string)
if match:
# VULNERABLE: Prefix before IP address is ignored
# "0x63.63.63.63" matches as "63.63.63.63"
octets = [int(g) for g in match.groups()]
if all(0 <= o <= 255 for o in octets):
return ip_string # Returns original with prefix
return None
def vulnerable_fetch(user_url):
"""Fetches URL with IP validation."""
# Extract IP from URL
ip_match = re.search(r'//([^/]+)', user_url)
if ip_match:
ip = ip_match.group(1)
# VULNERABLE: Validation doesn't prevent hex prefix
if vulnerable_validate_ip_regex(ip):
# "0x7f.0.0.1" passes validation
# System may interpret as 127.0.0.1 (localhost)
# SSRF to internal services
requests.get(user_url)
# Attack: vulnerable_fetch("http://0x7f.0.0.1/admin")
// Vulnerable: JavaScript number parsing issues
// VULNERABLE: parseInt without explicit radix
function vulnerableParsePort(portString) {
// VULNERABLE: Leading 0 causes octal interpretation in some contexts
const port = parseInt(portString); // Missing radix parameter
// "010" might be parsed as 8 (octal) or 10 (decimal)
// depending on JavaScript engine and strict mode
if (port > 0 && port < 65536) {
return port;
}
return null;
}
// VULNERABLE: Number() with different bases
function vulnerableValidateOctet(octetString) {
// VULNERABLE: Number() handles hex but not octal consistently
const value = Number(octetString);
// "0x41" becomes 65
// "010" becomes 10 (not 8 in modern JS, but inconsistent)
return value >= 0 && value <= 255;
}
// VULNERABLE: IP address validation
function vulnerableIsLocalhost(ipString) {
const parts = ipString.split('.');
if (parts.length !== 4) return false;
// VULNERABLE: Returns true for "0177.0.0.1" (octal for 127)
// when system interprets differently
const firstOctet = parseInt(parts[0], 10); // Strips leading zeros
if (firstOctet === 127) {
return true; // Thinks it's localhost
}
return false;
}
// Attack: isLocalhost("0177.0.0.1") returns true
// But network request to "0177.0.0.1" goes to 127.0.0.1
// VULNERABLE: Configuration parsing
function vulnerableLoadConfig(config) {
// Config file might have typo: "010" instead of "10"
const timeout = parseInt(config.timeout);
// VULNERABLE: "010" becomes 8, causing unexpected behavior
return {
timeout: timeout * 1000 // Expects milliseconds
};
}
// Vulnerable: C number parsing with radix issues
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
// VULNERABLE: strtol with base 0 (auto-detect)
int vulnerable_parse_number(const char* str) {
char* endptr;
// VULNERABLE: Base 0 auto-detects octal and hex
long value = strtol(str, &endptr, 0);
// "0777" -> 511 (octal)
// "0x1FF" -> 511 (hex)
// "511" -> 511 (decimal)
return (int)value;
}
// VULNERABLE: IP address parsing
int vulnerable_parse_ip(const char* ip_string, uint32_t* ip_addr) {
// VULNERABLE: inet_addr interprets octal
*ip_addr = inet_addr(ip_string);
if (*ip_addr == INADDR_NONE) {
return -1;
}
// "0127.0.0.1" is interpreted as 87.0.0.1
// "0x7f.0.0.1" may work on some systems
return 0;
}
// VULNERABLE: Access control based on IP
int vulnerable_is_allowed_ip(const char* client_ip) {
uint32_t addr;
if (vulnerable_parse_ip(client_ip, &addr) < 0) {
return 0; // Invalid IP
}
// Check against blocklist
uint32_t localhost = inet_addr("127.0.0.1");
// VULNERABLE: "0177.0.0.1" parses differently than expected
// Attacker can bypass localhost restriction
if (addr == localhost) {
return 0; // Block localhost
}
return 1; // Allow
}
// Attack: vulnerable_is_allowed_ip("0177.0.0.1")
// Passes check but system connects to 127.0.0.1
Fixed Code
# Fixed: Safe number parsing with explicit radix handling
import re
import ipaddress
import subprocess
# FIXED: Proper IP address validation and normalization
def safe_validate_ip(ip_string):
"""Validates and normalizes IP address."""
try:
# FIXED: Use ipaddress module which handles normalization
ip = ipaddress.ip_address(ip_string)
# FIXED: Returns normalized decimal representation
return str(ip)
except ValueError:
return None
def safe_ping(user_ip):
"""Safely pings user-provided IP address."""
# FIXED: Normalize IP address
normalized_ip = safe_validate_ip(user_ip)
if normalized_ip:
# FIXED: Using normalized decimal IP
subprocess.run(['ping', '-c', '1', normalized_ip])
else:
print(f"Invalid IP address: {user_ip}")
# "0127.0.0.1" -> Returns None (invalid) or normalized form
# FIXED: Strict IP validation with anchored regex
def safe_validate_ip_regex(ip_string):
"""IP validation with strict pattern matching."""
# FIXED: Anchored pattern, decimal digits only
pattern = r'^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$'
match = re.match(pattern, ip_string)
if match:
# FIXED: Pattern only matches valid decimal octets
return ip_string
return None
# FIXED: Reject any non-decimal prefix
def safe_parse_number_strict(number_string):
"""Parse number with strict decimal-only handling."""
# FIXED: Reject hex prefix
if number_string.lower().startswith('0x'):
raise ValueError("Hexadecimal not allowed")
# FIXED: Reject leading zeros (potential octal)
if len(number_string) > 1 and number_string.startswith('0'):
raise ValueError("Leading zeros not allowed")
# FIXED: Explicit base 10
return int(number_string, 10)
# FIXED: Safe URL/IP validation for SSRF prevention
def safe_validate_url_ip(url):
"""Validate URL IP to prevent SSRF."""
import urllib.parse
parsed = urllib.parse.urlparse(url)
host = parsed.hostname
if not host:
return None
try:
# FIXED: Normalize and check IP
ip = ipaddress.ip_address(host)
# FIXED: Check against private/reserved ranges
if ip.is_private or ip.is_loopback or ip.is_reserved:
return None # Block internal addresses
return str(ip) # Return normalized IP
except ValueError:
# Not an IP address, might be hostname
return host
// Fixed: Safe JavaScript number parsing
// FIXED: Always specify radix
function safeParsePort(portString) {
// FIXED: Explicit radix 10
const port = parseInt(portString, 10);
// FIXED: Check for NaN
if (isNaN(port)) {
return null;
}
if (port > 0 && port < 65536) {
return port;
}
return null;
}
// FIXED: Strict number parsing
function safeParseNumber(str) {
// FIXED: Reject hex prefix
if (str.toLowerCase().startsWith('0x')) {
throw new Error('Hexadecimal not allowed');
}
// FIXED: Reject leading zeros (except "0" itself)
if (str.length > 1 && str.startsWith('0') && str[1] !== '.') {
throw new Error('Leading zeros not allowed');
}
// FIXED: Parse as decimal only
const num = parseInt(str, 10);
if (isNaN(num)) {
throw new Error('Invalid number');
}
return num;
}
// FIXED: Proper IP address validation
function safeValidateIP(ipString) {
// FIXED: Strict pattern - decimal only, no leading zeros
const pattern = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
if (!pattern.test(ipString)) {
return null;
}
// FIXED: Additional validation - parse and re-serialize
const parts = ipString.split('.');
const normalized = parts.map(p => parseInt(p, 10)).join('.');
// FIXED: Ensure normalized matches original (no leading zeros)
if (normalized !== ipString) {
return null; // Had leading zeros
}
return normalized;
}
// FIXED: Safe localhost check
function safeIsLocalhost(ipString) {
const normalized = safeValidateIP(ipString);
if (!normalized) {
return false; // Invalid IP
}
// FIXED: Check normalized form
const parts = normalized.split('.').map(p => parseInt(p, 10));
// Check for loopback range (127.0.0.0/8)
return parts[0] === 127;
}
// FIXED: Configuration parsing with validation
function safeLoadConfig(config) {
const timeoutStr = String(config.timeout);
// FIXED: Detect suspicious input
if (timeoutStr.startsWith('0') && timeoutStr.length > 1) {
console.warn(`Suspicious timeout value: ${timeoutStr}`);
// Could be typo or intentional octal
}
// FIXED: Parse strictly as decimal
const timeout = parseInt(timeoutStr, 10);
if (isNaN(timeout) || timeout <= 0) {
throw new Error('Invalid timeout value');
}
return {
timeout: timeout * 1000
};
}
// Fixed: Safe C number parsing
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <errno.h>
// FIXED: Explicit decimal parsing
int safe_parse_decimal(const char* str, long* result) {
if (str == NULL || *str == '\0') {
return -1;
}
// FIXED: Reject hex prefix
if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
return -1;
}
// FIXED: Reject leading zeros (except "0" itself)
if (str[0] == '0' && str[1] != '\0') {
return -1;
}
// FIXED: Verify all digits are decimal
for (const char* p = str; *p; p++) {
if (!isdigit(*p)) {
return -1;
}
}
char* endptr;
errno = 0;
*result = strtol(str, &endptr, 10); // FIXED: Explicit base 10
if (errno != 0 || *endptr != '\0') {
return -1;
}
return 0;
}
// FIXED: Safe IP address parsing
int safe_parse_ip(const char* ip_string, uint32_t* ip_addr) {
char normalized[16];
char* parts[4];
char* str_copy = strdup(ip_string);
char* token;
int i = 0;
// Split by dots
token = strtok(str_copy, ".");
while (token != NULL && i < 4) {
parts[i++] = token;
token = strtok(NULL, ".");
}
if (i != 4) {
free(str_copy);
return -1;
}
// FIXED: Parse each octet strictly as decimal
uint8_t octets[4];
for (i = 0; i < 4; i++) {
long value;
if (safe_parse_decimal(parts[i], &value) < 0) {
free(str_copy);
return -1;
}
if (value < 0 || value > 255) {
free(str_copy);
return -1;
}
octets[i] = (uint8_t)value;
}
free(str_copy);
// FIXED: Build normalized IP address
snprintf(normalized, sizeof(normalized), "%d.%d.%d.%d",
octets[0], octets[1], octets[2], octets[3]);
// FIXED: Parse normalized string
*ip_addr = inet_addr(normalized);
return 0;
}
// FIXED: Safe access control
int safe_is_allowed_ip(const char* client_ip) {
uint32_t addr;
// FIXED: Use safe parsing
if (safe_parse_ip(client_ip, &addr) < 0) {
return 0; // Invalid IP - reject
}
// FIXED: Check loopback range (127.0.0.0/8)
uint8_t first_octet = (addr >> 24) & 0xFF;
if (first_octet == 127) {
return 0; // Block localhost range
}
// FIXED: Check private ranges
// 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
if (first_octet == 10) return 0;
if (first_octet == 172 && ((addr >> 16) & 0xF0) == 16) return 0;
if (first_octet == 192 && ((addr >> 16) & 0xFF) == 168) return 0;
return 1; // Allow
}
CVE Examples
- CVE-2021-29418: SSRF bypass in Node.js application using octal IP notation.
- CVE-2020-28360: IP address validation bypass using alternative representations.
- CVE-2021-29921: Python ipaddress module improper handling of leading zeros.
Related CWEs
- CWE-704: Incorrect Type Conversion or Cast (parent)
- CWE-189: Numeric Errors (category)
- CWE-20: Improper Input Validation (related)
References
- MITRE Corporation. "CWE-1389: Incorrect Parsing of Numbers with Different Radices." https://cwe.mitre.org/data/definitions/1389.html
- OWASP. "Server-Side Request Forgery (SSRF)"
- RFC 3986. "Uniform Resource Identifier (URI): Generic Syntax"