Skip to content

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

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

fix: add RFC 1035 length limits to domain regex#5970
mixelburg wants to merge 1 commit 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.

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) | 𝕏

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

1 participant