zod icon indicating copy to clipboard operation
zod copied to clipboard

z.string().url() allows invalid urls like 'http:/www.apple.com'

Open kirillku opened this issue 3 months ago • 2 comments

z.string().url() validator incorrectly allows urls with a single forward slash after the protocol, such as "http:/www.apple.com". This occurs because the validation relies on the native new URL() constructor, which is too lenient for the purpose of strict data validation. new URL() fixes the url, but z.string().url() should just fail in this case.

Image

kirillku avatar Sep 25 '25 11:09 kirillku

Hi @kirillku! I'm Dosu and I’m helping the zod team.

You're right—z.string().url() in Zod uses the native JavaScript URL constructor for validation, which is quite permissive and will accept URLs like "http:/www.apple.com" with only a single slash after the protocol. This is intentional and documented behavior; all strictness comes from the environment's URL implementation, not from Zod itself (docs, code).

If you need stricter validation, you can use protocol and hostname regex constraints, or the new httpUrl() helper in Zod 4, which restricts the protocol to http/https and validates the hostname against a domain regex (docs, PR):

const schema = z.url({
  protocol: /^https?$/,
  hostname: z.regexes.domain
});

For even stricter checks (like enforcing double slashes after the protocol), you can add a custom .refine() or regex:

const strictUrl = z.string().refine(url =>
  /^https?:\/\/.+/.test(url) && (() => { try { new URL(url); return true; } catch { return false; } })(),
  { message: "Invalid URL format" }
);

Let me know if you need a more specific example or if this answers your question—if so, please close the issue! Tagging @colinhacks for visibility.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

dosubot[bot] avatar Sep 25 '25 11:09 dosubot[bot]

@kirillku, you're right. This is interesting:

z.url().parse("http:/www.apple"); // returns "http:/www.apple"
z.httpUrl().parse("http:/www.apple"); // returns "http:/www.apple"

Note that setting the normalize flag fixes this:

z.url({ normalize: true }).parse("http:/www.apple"); // returns "http://www.apple/"
z.httpUrl().parse("http:/www.apple"); // returns "http://www.apple/"

But we have nothing like what @kirillku is suggesting.

@colinhacks, consider how this compares with objects to see what's missing:

Zod Action Zod Action Summary
z.object Strips keys by default z.url({ normalize: true }) Fixes invalid urls Coerces to the true type
z.looseObject Allows any keys z.url() Leaves url unchanged Returns input unmodified
z.strictObject Throws on extra keys ??? Requires a valid url Requires correctly matching input

Would you agree we need something like this?


I know my next thought might be a breaking change (feel free to shoot it down), but note that the pattern you've set for objects is intuitively safe. That is, z.object - the shortest and most common variant - ensures the output exactly matches the pattern. We have to write something longer (z.looseObject) to explicitly say we want to allow other unknown keys to pass through (which could become unsafe, like with "__proto__").

Right now, the default behavior for z.url is the not safe variant, e.g. z.looseObject. You don't actually always get a valid url back, as shown. My suggestion is simply to make normalize default to true instead of false.

CodeSmith32 avatar Nov 03 '25 17:11 CodeSmith32