“Validate this string as an IP address.” Sounds trivial. Then you discover IPv4 has at least five textual representations, IPv6 has even more, and the regex you copied from Stack Overflow has been silently passing strings like 999.999.999.999. IP validation is a place where naive code is wrong in subtle ways.
This post covers IP address validation properly: what forms IPv4 and IPv6 take, why regex is rarely the right tool, the standard library functions in major languages, and the edge cases your validation should care about.
Why Validation Is Trickier Than It Looks
A few classic gotchas:
IPv4 has multiple textual forms
- Standard dotted-decimal:
192.168.1.1 - Decimal (long):
3232235777(the same address as 32-bit integer) - Hexadecimal:
0xC0A80101 - Octal:
0300.0250.0001.0001 - Mixed:
192.0xa8.1.1
Some library functions accept all of these; some accept only the first. Some applications silently exploit the parsing inconsistency.
IPv6 has even more
- Full:
2001:0db8:0000:0000:0000:0000:0000:0001 - Shortened:
2001:db8::1 - Embedded IPv4:
::ffff:192.0.2.1 - Zone ID:
fe80::1%eth0(link-local with interface specifier)
Comparing 2001:db8::1 to 2001:0db8::0001 for equality requires normalization.
Leading zeros
192.168.001.001 looks fine but some libraries treat leading zeros as octal — 001 is 1, but 010 is 8. This is the source of real CVEs.
Whitespace
" 192.168.1.1" and "192.168.1.1 " — does your validator strip? Reject? Depends on library.
For all these reasons, don’t write your own regex. Use library validators.
The Right Tool in Each Language
Node.js
The built-in net module:
import net from 'node:net'
net.isIP('192.168.1.1') // 4
net.isIP('2001:db8::1') // 6
net.isIP('999.999.999.999') // 0 (invalid)
net.isIPv4('192.168.1.1') // true
net.isIPv6('2001:db8::1') // true
Returns the version (4 or 6) or 0 for invalid. Conservative — rejects octal/hex/decimal forms.
Python
The ipaddress module (stdlib):
import ipaddress
ipaddress.ip_address('192.168.1.1') # IPv4Address('192.168.1.1')
ipaddress.ip_address('2001:db8::1') # IPv6Address('2001:db8::1')
try:
ipaddress.ip_address('999.999.999.999')
except ValueError:
print('invalid')
Strict by default. The ip_address() constructor raises ValueError on invalid input.
Go
The net package:
import "net"
ip := net.ParseIP("192.168.1.1")
if ip == nil {
// invalid
}
if ip.To4() != nil {
// it's IPv4
}
Java
java.net.InetAddress:
try {
InetAddress addr = InetAddress.getByName(input);
// valid
} catch (UnknownHostException e) {
// invalid (or DNS lookup failed — note: getByName does DNS!)
}
Warning: InetAddress.getByName() does DNS resolution for non-IP strings. Use InetAddresses.forString() from Guava or InetAddress.getByAddress() with byte arrays for pure validation.
PHP
filter_var with the FILTER_VALIDATE_IP filter:
filter_var('192.168.1.1', FILTER_VALIDATE_IP); // returns the IP if valid
filter_var('192.168.1.1', FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); // only IPv4
filter_var('192.168.1.1', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE); // reject private
Rust
The standard library:
use std::net::IpAddr;
let addr: Result<IpAddr, _> = "192.168.1.1".parse();
match addr {
Ok(ip) => /* valid */,
Err(_) => /* invalid */,
}
Why use the library
The library handles edge cases consistently. Your regex won’t. Production-grade IP validation should always go through a library function.
What “Valid” Means in Your Context
Beyond “is this a syntactically correct IP,” you usually want additional checks:
Public vs private
Reject RFC 1918 addresses if you only want public IPs. Reject public IPs if you want only internal hosts.
ip = ipaddress.ip_address('192.168.1.1')
if ip.is_private:
raise ValueError('private IP not allowed')
Specific ranges
Want to ensure the IP is in 203.0.113.0/24?
network = ipaddress.ip_network('203.0.113.0/24')
ip = ipaddress.ip_address(user_input)
if ip not in network:
raise ValueError('outside allowed range')
Routable on the internet
A public IP that’s not loopback, not multicast, not reserved.
if ip.is_global:
# it's routable on the public internet
IPv4 only / IPv6 only
Depending on your application:
if not isinstance(ip, ipaddress.IPv4Address):
raise ValueError('IPv4 only')
Common Validation Mistakes
Regex like ^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$
Accepts 999.999.999.999 because \d{1,3} matches up to 999, not 0-255. This pattern is everywhere on the internet and almost always wrong.
Splitting and converting to int
ip.split('.').every(n => Number(n) >= 0 && Number(n) <= 255)
Misses: leading zeros, hex/octal interpretation by JavaScript, negative numbers ("-1" → -1), NaN quirks, extra parts ("1.2.3.4.5").
Trusting user-supplied IPs
If a user sends an X-Forwarded-For header with a syntactically valid IP, it doesn’t mean the IP is real or the one they’re really coming from. See X-Forwarded-For header.
Not normalizing for comparison
2001:db8::1 and 2001:0db8:0000:0000:0000:0000:0000:0001 are the same address. Use library == after parsing, not string ==.
ip1 = ipaddress.ip_address('2001:db8::1')
ip2 = ipaddress.ip_address('2001:0db8:0000:0000:0000:0000:0000:0001')
ip1 == ip2 # True
Forgetting IPv6
A validator that only accepts IPv4 will silently fail for IPv6 users. In 2026, IPv6 is meaningful traffic. Don’t write IPv4-only validators.
Validation in Web Forms
For user input (e.g., entering an IP for lookup):
- Strip whitespace before validating.
- Validate with library (not regex).
- Reject explicitly with a helpful error message if invalid.
- Normalize to canonical form before display/storage.
- Don’t trust the IP for security decisions if it came from the user.
function validateIpInput(raw: string): { valid: boolean, normalized?: string, error?: string } {
const trimmed = raw.trim()
const version = net.isIP(trimmed)
if (version === 0) return { valid: false, error: 'Invalid IP address' }
return { valid: true, normalized: trimmed }
}
Geolocation and Validation
A request to a geolocation API requires a valid IP. The API will validate on its side too, but client-side validation:
- Avoids unnecessary API calls for clearly bad input.
- Gives users immediate feedback.
- Prevents URL injection if the IP is in the URL path.
The Ip2Geo API returns an error for invalid IPs. Your client code should still validate first to provide better UX.
Performance Considerations
Library IP validation is fast — typically sub-microsecond per call. Don’t optimize prematurely with regex. The library is fast enough for any reasonable use case.
If you’re validating millions of IPs per second (unusual): batch them through a stream-friendly parser like ipaddress in Python or netaddr in Go.
The Regex (If You Insist)
For completeness, a correct IPv4 regex:
^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$
For IPv6, the correct regex is over 100 characters of arcane patterns. Don’t write it. Use a library.
TL;DR
- Use library validators, not regex.
net.isIP,ipaddress.ip_address,net.ParseIP, etc. - IPv4 has multiple legal forms (dotted, decimal, hex, octal). Most libraries accept only the standard form.
- IPv6 normalization matters for comparison.
::1and0:0:0:0:0:0:0:1are the same. - Validate context beyond syntax: public vs private, routable, specific range, IPv4 vs IPv6.
- Don’t trust user-supplied IPs for security; validate syntax but don’t trust origin.
- Leading zeros in IPv4 can be interpreted as octal — a real security risk in naive parsers.
- For UX, validate client-side; API validates server-side too.
IP validation is one of those tasks that looks simple and isn’t. The good news: every modern language has a library function that gets it right. Use them. For the broader IP-format picture, see everything you need to know about IP addresses; for the IPv4-to-IPv6 transition that means you must accept both, IPv4 vs IPv6 transition.