Skip to content

Commit 3cd45eb

Browse files
LuckySilver0021colinhackscursoragent
authored
fix (v4) : adds strict validation to httpUrl() (#5672)
* checkpoint * refactored to make it robust * refactor: use shared httpProtocol regex constant instead of regex-inspecting regex source - Move /^https?$/ to regexes.httpProtocol shared constant - Compare def.protocol.source exactly against the constant - Only enforce :// check when normalize is off Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Colin McDonnell <colinmcd94@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 3a818de commit 3cd45eb

File tree

5 files changed

+27
-2
lines changed

5 files changed

+27
-2
lines changed

packages/zod/src/v4/classic/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ export function url(params?: string | core.$ZodURLParams): ZodURL {
516516

517517
export function httpUrl(params?: string | Omit<core.$ZodURLParams, "protocol" | "hostname">): ZodURL {
518518
return core._url(ZodURL, {
519-
protocol: /^https?$/,
519+
protocol: core.regexes.httpProtocol,
520520
hostname: core.regexes.domain,
521521
...util.normalizeParams(params),
522522
});

packages/zod/src/v4/classic/tests/string.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,12 @@ test("httpurl", () => {
454454
).toThrow();
455455
expect(() => httpUrl.parse("http://asdf.c")).toThrow();
456456
expect(() => httpUrl.parse("mailto:asdf@lckj.com")).toThrow();
457+
// missing // after protocol
458+
expect(() => httpUrl.parse("http:example.com")).toThrow();
459+
expect(() => httpUrl.parse("https:example.com")).toThrow();
460+
// missing one /
461+
expect(() => httpUrl.parse("https:/www.google.com")).toThrow();
462+
expect(() => httpUrl.parse("http:/example.com")).toThrow();
457463
});
458464

459465
test("url error overrides", () => {

packages/zod/src/v4/core/regexes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export const hostname: RegExp =
8181

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

84+
export const httpProtocol: RegExp = /^https?$/;
85+
8486
// https://blog.stevenlevithan.com/archives/validate-phone-number#r4-3 (regex sans spaces)
8587
// E.164: leading digit must be 1-9; total digits (excluding '+') between 7-15
8688
export const e164: RegExp = /^\+[1-9]\d{6,14}$/;

packages/zod/src/v4/core/schemas.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,23 @@ export const $ZodURL: core.$constructor<$ZodURL> = /*@__PURE__*/ core.$construct
478478
try {
479479
// Trim whitespace from input
480480
const trimmed = payload.value.trim();
481+
482+
// When normalize is off, require :// for http/https URLs
483+
// This prevents strings like "http:example.com" or "https:/path" from being silently accepted
484+
if (!def.normalize && def.protocol?.source === regexes.httpProtocol.source) {
485+
if (!/^https?:\/\//i.test(trimmed)) {
486+
payload.issues.push({
487+
code: "invalid_format",
488+
format: "url",
489+
note: "Invalid URL format",
490+
input: payload.value,
491+
inst,
492+
continue: !def.abort,
493+
});
494+
return;
495+
}
496+
}
497+
481498
// @ts-ignore
482499
const url = new URL(trimmed);
483500

packages/zod/src/v4/mini/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export function url(params?: string | core.$ZodURLParams): ZodMiniURL {
200200
// @__NO_SIDE_EFFECTS__
201201
export function httpUrl(params?: string | Omit<core.$ZodURLParams, "protocol" | "hostname">): ZodMiniURL {
202202
return core._url(ZodMiniURL, {
203-
protocol: /^https?$/,
203+
protocol: core.regexes.httpProtocol,
204204
hostname: core.regexes.domain,
205205
...util.normalizeParams(params),
206206
});

0 commit comments

Comments
 (0)