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

ImpactDetails
ConfidentialityScope: Confidentiality

Attackers may access private resources using unexpected numerical bases.
IntegrityScope: 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.

  • CWE-704: Incorrect Type Conversion or Cast (parent)
  • CWE-189: Numeric Errors (category)
  • CWE-20: Improper Input Validation (related)

References

  1. MITRE Corporation. "CWE-1389: Incorrect Parsing of Numbers with Different Radices." https://cwe.mitre.org/data/definitions/1389.html
  2. OWASP. "Server-Side Request Forgery (SSRF)"
  3. RFC 3986. "Uniform Resource Identifier (URI): Generic Syntax"