Check whether your own IP addresses are valid

Paste the addresses you actually have — from a log file, a firewall rule, a config file — into the tester below and see which ones are genuinely valid IPv4 and which only look like it. The sample already loaded mixes four valid addresses with three that fail, including two a shorter, more common pattern would wrongly accept. The copy-paste pattern, explained token by token, and the question of what counts as routable or private, follow underneath.

/ / gm
Flags

    Your pattern and test text never leave this tab. Matching runs in a same-origin Web Worker with a timeout, so an internal IP address from a log file is never sent anywhere. That protection matters more than it looks: a single regular expression once caused a real, global outage — the timeout above is what keeps this page safe from the same failure mode.

    ^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$

    Why not just \d{1,3}(\.\d{1,3}){3}?

    Because that shorter, more common pattern accepts 999.999.999.999, 256.1.1.1 and 300.0.0.1 — none of them a valid IPv4 address, since every octet is an 8-bit number and must be between 0 and 255 inclusive. “One to three digits” is a much weaker constraint than “0 to 255”, and the gap between them is exactly the kind of thing that passes a quick manual test (192.168.1.1 works fine) and then quietly accepts garbage in production. The pattern on this page is longer specifically to close that gap.

    Token by token

    The whole pattern is one octet-matching group, reused four times. The octet group itself is four alternatives, tried in order, each covering one slice of 0–255:

    AlternativeCovers
    25[0-5]250255
    2[0-4][0-9]200249
    1[0-9]{2}100199
    [1-9]?[0-9]099, with no leading zero required or permitted for the single-digit case

    Alternation in JavaScript regex tries each branch left to right and takes the first one that matches, so ordering matters here: 25[0-5] must come before 2[0-4][0-9], or a number like 253 would never reach the branch that can actually match it, because a looser branch tried first can consume the wrong number of characters and leave the rest of the pattern unable to continue. (In this specific case the branches are mutually exclusive by their leading digits, so the order is for correctness of intent more than a live bug — but it is the right habit for alternation in general.)

    The full octet group is wrapped as (…) and then reused: (\.(…)){3} repeats “a literal dot, then another octet” exactly three times, giving four octets in total — one at the start, three more after a dot each.

    What it matches

    • 192.168.1.1 — a private-range address, still a syntactically valid one.
    • 0.0.0.0 and 255.255.255.255 — both boundary values of the valid range, both accepted.
    • 8.8.8.8 — an ordinary public address.

    What it deliberately does not match, and why

    InputVerdictWhy
    256.1.1.1Rejected256 is one past the maximum octet value of 255 — this is the whole reason the pattern is this long instead of a simple \d{1,3}
    1.2.3RejectedOnly three octets, not four
    999.999.999.999RejectedEvery octet is out of range
    192.168.1.1.1RejectedFive octets, not four — the anchors mean a fifth trailing group breaks the match entirely rather than being silently ignored
    192.168.001.1AcceptedLeading zeros are not rejected by this pattern. Historically, some systems (notably many implementations of the C standard library’s inet_aton) treat a leading-zero octet as octal, so 0100 means decimal 64, not 100 — a discrepancy that has been used in real server-side request forgery (SSRF) bypasses. If your validator needs to guard against this, reject any octet with a leading zero unless it is exactly "0"

    What “valid IPv4 syntax” does not tell you

    This pattern answers exactly one question: is this four dot-separated numbers, each in range? It says nothing about whether the address is routable on the public internet (many ranges, like 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16, are reserved for private networks and never routed publicly), whether it is a loopback address (127.0.0.0/8), whether it is currently assigned to anything, or whether a host actually responds at that address. Classifying an address into public, private, loopback, link-local or multicast requires comparing the parsed number against known ranges — a lookup table, not a regex — and confirming reachability requires an actual network request (a ping, a socket connection), which this client-side pattern deliberately does not attempt.

    Flavour: this is JavaScript’s regex

    This pattern targets JavaScript’s built-in RegExp, per ECMA-262. It uses only literals, character classes, {n} quantifiers and alternation — every one of those works identically in PCRE and Python’s re, so this exact pattern (including the {2} quantifier syntax) needs no changes to move between them. See the cookbook’s flavour-honesty section for constructs that do not port cleanly.

    Worked examples

    1. Distinguishing IPv4 from IPv6 in a mixed log

    A server log recording client addresses may contain both 203.0.113.4 and 2001:db8::1 on different lines. This pattern matches only the first shape — IPv6 addresses use colons and hexadecimal groups entirely unlike this pattern’s dot-and-decimal structure, so they simply do not match, which is the correct behaviour if you specifically need to separate the two rather than validate either.

    2. Redacting internal IPs before sharing a log externally

    With g on and the Replace tab, this pattern finds every IPv4-shaped string in a block of text. Combined with a check against the private ranges mentioned above (done separately, in code, after matching), you can redact only internal addresses and leave public ones visible — the regex finds candidates, the range check decides which ones to act on.

    3. Why 192.168.001.001 is a bug waiting to happen

    Two systems that both accept leading-zero octets can still disagree about what the address means: one may pass it straight to a system call that interprets 010 as octal 8, decimal 8, while another treats it as literal decimal 10. The mismatch has been used to make an address that looks like it points to a public host pass a validator and then actually resolve to an internal one when handed to the operating system’s network stack. If your validator’s job includes security (blocking access to internal addresses, for instance), reject leading zeros explicitly rather than relying on this pattern’s permissiveness.

    4. Validating a CIDR range, not just a bare address

    A firewall rule or an allowlist entry is often written as 192.168.1.0/24 rather than a bare address — an IPv4 address, a literal slash, then a prefix length from 0 to 32. This pattern covers only the address portion. Extending it means adding (?:\/(?:3[0-2]|[12]?[0-9]))? after the existing pattern, as an optional suffix — itself a small range-correct group in the same style as the octet groups above, since a prefix length also has a hard numeric ceiling (32) that a loose \d{1,2} would not enforce.

    Using this pattern in your own code

    Every construct here — character classes, alternation, {n} quantifiers — is standard across mainstream regex engines, so the pattern text itself does not change between a browser, a server script, or the shell.

    // JavaScript
    const IPV4_RE = /^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/;
    IPV4_RE.test('192.168.1.1');  // true
    IPV4_RE.test('256.1.1.1');    // false
    # Python's re module — identical pattern text
    import re
    IPV4_RE = re.compile(
        r'^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])'
        r'(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$'
    )
    bool(IPV4_RE.match('8.8.8.8'))  # True
    # For real work, most languages also ship a dedicated address parser that is
    # a better fit than any regex once you need to reason about ranges:
    python3 -c "import ipaddress; print(ipaddress.ip_address('192.168.1.1').is_private)"
    # -> True

    That last line is worth taking seriously as advice, not just a footnote: once the actual question is “is this address private, loopback, or otherwise special” rather than “is this text shaped like four octets”, a real IP-address library (Python’s ipaddress module, Node’s net module, or equivalents in other languages) already knows every reserved range correctly and will not need to be manually kept in sync with future allocations the way a hand-written regex would.

    Common mistakes when adapting this pattern

    • Reordering the alternatives inside the octet group. The order 25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9] is deliberate: each branch is tried left to right, and putting a broader branch like [1-9]?[0-9] first would still work correctly here only because the branches are each disambiguated by their leading digit — but as a general habit with alternation, more specific branches belong before more general ones, since a general branch tried first can consume characters a later, more specific branch needed.
    • Assuming this pattern also validates a subnet mask or CIDR suffix. 192.168.1.1/24 fails this pattern outright, because of the trailing $ anchor right after the fourth octet. See worked example 4 above for the extension that adds an optional CIDR suffix, rather than assuming the base pattern already handles it.
    • Trying to also validate IPv6 by adding colons to the character classes. IPv6’s address grammar (hexadecimal groups, the :: zero-compression shorthand, embedded IPv4 tails in some notations) is different enough from IPv4’s that no small edit to this pattern gets there — it needs its own pattern, built around IPv6’s own rules, not a patched version of this one.
    • Forgetting that whitespace inside the address is never valid. A pasted address sometimes carries a stray space from copy-paste — 192.168. 1.1 — and the anchored pattern correctly rejects it rather than silently trimming it. Call value.trim() and, if needed, strip internal whitespace explicitly before testing, rather than loosening the pattern itself to tolerate spaces that should not be there in the first place.

    Frequently asked questions

    Does this match IPv6 addresses too?

    No. IPv6 has an entirely different textual format — eight groups of hexadecimal digits separated by colons, with a :: shorthand for runs of zeros — and needs its own pattern, considerably more involved than this one, to handle that shorthand correctly.

    Why does 192.168.001.1 pass, if leading zeros are a known problem?

    Because rejecting them is a deliberate additional constraint, not a default one, and different applications want different behaviour here. See the table above for the specific SSRF-adjacent risk, and add (?!0\d) as a negative lookahead before each octet group if you need to reject them.

    Does a match mean the address is routable on the internet?

    No. Routability depends on whether the address falls in a reserved private, loopback, link-local or multicast range, which this pattern does not check. See “what ‘valid IPv4 syntax’ does not tell you” above for the full list of ranges to check separately if that distinction matters to you.

    Can I capture each octet separately?

    The pattern as written only captures the last octet distinctly per repetition, because the octet group is nested inside the repeated (\.(…)){3} structure — JavaScript keeps only the final iteration’s capture for a group inside a quantified group. To capture all four separately, write out four named or numbered groups explicitly rather than reusing one group with a quantifier.

    Why 0–255 and not 1–255, since 0.0.0.0 seems unusual?

    0.0.0.0 is a valid IPv4 address with defined special meanings (a default route, or “this host”, depending on context), so it is correctly included in the syntactic range even though it is unusual to see as a destination address in practice.

    Why do I see four separate matches for one address in the highlighted view?

    You should not, for a single address on its own line — the whole thing (192.168.1.1) is one match, with the repeated octet group contributing to the count internally but not producing separate top-level matches. If a test string has several addresses on separate lines, each line produces its own match, which is the g flag doing exactly what it is meant to: find every occurrence, not just the first.

    Is my IP address sent anywhere when I test it here?

    No. Matching runs inside a same-origin Web Worker, and there is no upload endpoint anywhere on this page — open the Network panel in developer tools and watch it stay empty while you paste addresses in.

    Limitations

    • IPv4 only — no IPv6 support.
    • Does not reject leading zeros, which some systems interpret as octal; add (?!0\d) per octet if that matters for your use case.
    • Does not classify the address as private, loopback, link-local or public — that needs a separate range check against the parsed numeric value.
    • Cannot confirm reachability or that anything is actually listening at the address — that needs a real network request, out of scope for a client-side pattern.

    Related