Skip to content

fix: add RFC 1035 length limits to domain regex - #5970

Open
mixelburg wants to merge 4 commits into
colinhacks:mainfrom
mixelburg:fix/domain-rfc1035-length-limits
Open

fix: add RFC 1035 length limits to domain regex#5970
mixelburg wants to merge 4 commits into
colinhacks:mainfrom
mixelburg:fix/domain-rfc1035-length-limits

Conversation

@mixelburg

Copy link
Copy Markdown
Contributor

Problem

The regexes.domain regex (used internally by z.httpUrl() for hostname validation) was missing RFC 1035 length limits that the regexes.hostname regex already enforces:

  • No 253-character total length limit on the domain
  • No upper bound on TLD label length ({2,} vs {2,63})

This meant z.httpUrl() accepted domains exceeding the 253-character DNS limit and TLDs longer than 63 characters, while z.hostname() correctly rejected them.

Fix

Added RFC 1035 length constraints to regexes.domain:

  • (?=.{1,253}\.?$) lookahead — enforces the 253-character total domain length limit
  • {2,63} on the TLD label — caps the last label at 63 characters per RFC 1035

Testing

  • All existing 3811 tests pass across 339 test files
  • Added 2 new test cases: TLD >63 chars rejected, domain >253 chars rejected

Fixes #5965

@pullfrog

pullfrog Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

TL;DR — Aligns regexes.domain with regexes.hostname by adding the RFC 1035 length limits it was missing, so z.httpUrl() rejects domains over 253 characters and TLDs over 63 characters. Fixes #5965.

Key changes

  • Enforce 253-char domain total in regexes.domain — adds the (?=.{1,253}\.?$) lookahead that regexes.hostname already uses.
  • Cap TLD label at 63 chars — tightens the final label quantifier from {2,} to {2,63} per RFC 1035.
  • Cover both limits in string.test.ts — new httpurl cases assert rejection of a 64-char TLD and a >253-char domain.

Summary | 2 files | 1 commit | base: mainfix/domain-rfc1035-length-limits


RFC 1035 length limits on regexes.domain

Before: z.httpUrl() accepted inputs like http://example.<64 chars> and 4×63-label domains exceeding the 253-char DNS cap, while z.hostname() rejected the same inputs.
After: regexes.domain matches regexes.hostname's length behavior, so z.httpUrl() hostname validation rejects over-long domains and TLDs.

The divergence existed because regexes.hostname was tightened with the {1,253} lookahead and {2,63} TLD cap but regexes.domain — used internally by z.httpUrl() — was not updated in step. This change brings the two regexes to parity on length constraints; label-character rules are unchanged.

regexes.ts · string.test.ts

Pullfrog  | View workflow run | via Pullfrog | Using Claude Opus𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The regex change itself is correct and well-motivated — RFC 1035 interpretations are right, no ReDoS risk, no incidental narrowing beyond the documented limits. But tightening z.regexes.domain leaves two spots in the tree that embed the old regex literal and will now be inaccurate: the recommended-schema callout in packages/docs/content/api.mdx and an inline comment next to the consumer in this test file. Please update both before merge.

Note that z.regexes.domain is a public export and this is technically a breaking change, though the newly-rejected inputs (TLD >63 chars, total domain >253 chars) are all DNS-invalid by RFC 1035 — the same limits z.regexes.hostname already enforces — so the framing as a fix is reasonable. Worth a line in the eventual release notes.


Note: 1 inline comment(s) dropped because they did not anchor to lines inside the PR diff:

  • packages/zod/src/v4/classic/tests/string.test.ts:433 (RIGHT) — line 433 (RIGHT) is not inside a diff hunk

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

/^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;

export const domain: RegExp = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
export const domain: RegExp = /^(?=.{1,253}\.?$)([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider a one-line annotation so the magic numbers are self-documenting, matching the style of the hostname comment above:

/** RFC 1035: labels ≤63 chars, total ≤253 chars, TLD 2–63 chars. */
export const domain: RegExp = /^(?=.{1,253}\.?$)([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;

Not blocking — but would save a reader from deriving intent from the literals.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion — added the RFC 1035 annotation in 3df7fb6.

The domain regex (used by z.httpUrl()) was missing length anchors that
the hostname regex already enforces:
- No 253-character total length limit
- No upper bound on TLD label length

Added:
- (?=.{1,253}\.?$) lookahead for overall domain length
- {2,63} TLD label cap (was unbounded {2,})

Fixes colinhacks#5965

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed changes — added two test cases exercising the new RFC 1035 length limits on z.regexes.domain.

  • TLD >63 chars rejectedhttp://example. + 64-char label triggers the {2,63} TLD cap.
  • Domain >253 chars rejected — 259-char hostname triggers the (?=.{1,253}\.?$) lookahead.

The two items flagged in the prior review (packages/docs/content/api.mdx:382 embedded regex literal and packages/zod/src/v4/classic/tests/string.test.ts:433 stale regex comment) remain unaddressed — both still show the pre-change pattern.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@mixelburg

Copy link
Copy Markdown
Contributor Author

Hi! Is there anything else needed from my side on this PR?

Sync the api.mdx documentation and test comment with the actual
z.regexes.domain pattern (RFC 1035 length limits: total ≤ 253, TLD ≤ 63).
@mixelburg

Copy link
Copy Markdown
Contributor Author

Updated the two stale regex references:\n- packages/docs/content/api.mdx:382 — sync'd embedded regex literal with the current z.regexes.domain pattern\n- packages/zod/src/v4/classic/tests/string.test.ts:433 — updated comment regex to match the new RFC 1035 limits

@wahajahmed010 wahajahmed010 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Nice catch — the inconsistency between regexes.hostname (already RFC 1035-compliant) and regexes.domain was a latent footgun in z.httpUrl()'s default. Aligning them is the right move.

Substantive notes:

  • (?=.{1,253}\.?$) is correct for the total-length constraint. . matches everything except newline by default in JS (no s flag needed here, no m flag set), so embedded newlines correctly fail the lookahead and the whole regex. Good.

  • {2,63} on the TLD matches RFC 1035 §2.3.4 (label length 1–63 octets). With the leading label constraint {0,61} from the rest of the regex, all labels are also capped — consistent with hostname.

  • Behavioural change worth flagging: any caller passing a string longer than 253 chars or with a >63-char label that was previously accepted by z.string().regex(z.regexes.domain) will now fail validation. That's the correct behaviour, but it's still a change — worth a CHANGELOG entry under zod/v4. If the project does a minor bump soon, an [next] note is appropriate.

  • Test additions cover the two failure modes (TLD >63 chars, total >253 chars). Suggest two more boundary cases to lock the contract:

    • exactly 63-char TLD → should pass ({2,63} inclusive upper bound)
    • exactly 253-char total length → should pass ({1,253} inclusive)

    These guard against off-by-one if anyone ever changes the quantifiers.

  • One small nit on the diff: the JSDoc comment /** RFC 1035: labels ≤63 chars, total ≤253 chars, TLD 2–63 chars. */ slightly undersells what the regex enforces — the leading label is also bounded to [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? (≤63 chars), same as hostname. Consider matching that wording for consistency, or just point readers at regexes.hostname's comment.

Otherwise solid. Approving once the boundary-case tests land or in a follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

regexes.domain is missing RFC 1035 length limits

2 participants