Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: URL validator #1

Merged
merged 16 commits into from
Jul 17, 2024
Merged

feat: URL validator #1

merged 16 commits into from
Jul 17, 2024

Conversation

zeyu2001
Copy link
Contributor

UrlValidator class

Copy link

@spaceraccoon spaceraccoon left a comment

Choose a reason for hiding this comment

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

Some thoughts on usage and possible workarounds!

Comment on lines +48 to +52
try {
router.push(validator.parse(redirectUrl))
} catch (error) {
router.push('/home')
}

Choose a reason for hiding this comment

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

Most validators use more of an if ... else pattern based on a boolean return value; is there a strong reason to use the try catch pattern instead? I guess one advantage is more meaningful error messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I also wanted to return a parsed version of the URL, which might be safer than saying OK to the string URL and letting them re-use it in e.g. router.push() - in the Isomer case, it was something like if isRelativeUrl(url) then router.push(url), but the Next.js weirdness could potentially have been avoided if parse returns a sane URL and it was router.push(parse(url)).

If we want to make it a parser + validator in that it returns the parsed result, I don't really like the "returning both the result and status" pattern because it doesn't force devs to handle errors, but throwing exceptions do. Maybe one option would be to do something like zod's parse and safeParse and let people choose?

Choose a reason for hiding this comment

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

Makes sense! I think stick to a single implementation i.e. the try catch pattern.

import { UrlValidationError } from '@/url/errors'
import { Whitelist } from '@/url/options'

const DYNAMIC_ROUTE_SEGMENT_REGEX = /\[\[?([^\]]+)\]?\]/g

Choose a reason for hiding this comment

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

Where is this from? Might be more true-to-implementation by using Next.js' regex exactly https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/route-regex.ts#L29C27-L29C48

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This regex is weird because it results in a greedy match of e.g.

[test][test]
 <-------->

I think they're mostly the same functionally except I've moved the capturing group inside and used [^\]] to match all but ] to avoid the greedy matching. Maybe best way is still to block weird chars in pathname or urlencode it

let normalizedUrl: URL

try {
normalizedUrl = new URL(url, baseUrl)

Choose a reason for hiding this comment

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

Is there a potential footgun here in which a user expects that they can provide a baseUrl with a path e.g.

> new URL('/123', 'https://example.com/user/')
< URL {href: "https://example.com/123", origin: "https://example.com", protocol: "https:", username: "", password: "", …}

This also opens it up to traversals.

> new URL('./123', 'https://example.com/user/test')
URL {
  href: 'https://example.com/user/123',
  origin: 'https://example.com',
  protocol: 'https:',
  username: '',
  password: '',
  host: 'example.com',
  hostname: 'example.com',
  port: '',
  pathname: '/user/123',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
}
> new URL('../123', 'https://example.com/user/test')
URL {
  href: 'https://example.com/123',
  origin: 'https://example.com',
  protocol: 'https:',
  username: '',
  password: '',
  host: 'example.com',
  hostname: 'example.com',
  port: '',
  pathname: '/123',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Enforcing url.pathname === '/' for baseOrigin

packages/validators/src/url/utils.ts Outdated Show resolved Hide resolved
return normalizedUrl
}
/* As of Next.js 14.2.5, router.push() resolves dynamic routes using query parameters. */
const resolveNextDynamicRoute = (url: URL): URL => {

Choose a reason for hiding this comment

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


Validates URLs against a whitelist of allowed protocols and hostnames, preventing open redirects, XSS, SSRF, and other security vulnerabilities.

### new UrlValidator(options)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add this as TSDoc comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks boss ur the best

@zeyu2001 zeyu2001 merged commit e5b219c into develop Jul 17, 2024
3 checks passed
@zeyu2001 zeyu2001 deleted the feat/url-validator branch July 17, 2024 08:03
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.

3 participants