From 79659a7fc4886475c4f9f12892f264a5dbf5c332 Mon Sep 17 00:00:00 2001 From: Giovani Date: Fri, 25 Jul 2025 04:41:35 +0000 Subject: [PATCH] feat: add proper validation and normalization on input --- frontend/src/pages/Home.tsx | 16 ++++++- frontend/src/utils/url.ts | 87 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 frontend/src/utils/url.ts diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 0b159ad..5c25cd8 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { useAppDispatch, useAppSelector } from '../app/hooks'; import { copyToClipboard } from '../utils/clipboard'; +import { normalizeUrl, isValidUrlForShortening } from '../utils/url'; import { shortenUrl, selectShortUrl, @@ -12,10 +13,10 @@ import { const Home: React.FC = () => { /* component level state */ const [url, setUrl] = useState(''); + const [urlError, setUrlError] = useState(''); /* global state */ const dispatch = useAppDispatch(); - const shortUrl = useAppSelector(selectShortUrl); const status = useAppSelector(selectUrlStatus); const error = useAppSelector(selectUrlError); @@ -29,7 +30,13 @@ const Home: React.FC = () => { const handleSubmit = () => { if (url.trim() !== '') { - dispatch(shortenUrl(url)); + if (!isValidUrlForShortening(url)) { + setUrlError('Hmm, that URL doesn’t look right. Try something like https://example.com.'); + dispatch(clearShortUrl()) + } else { + setUrlError(''); + dispatch(shortenUrl(normalizeUrl(url))); + } } }; @@ -42,6 +49,7 @@ const Home: React.FC = () => { }; const handleClear = () => { + setUrl('') dispatch(clearShortUrl()) }; @@ -68,6 +76,10 @@ const Home: React.FC = () => { + {urlError && ( +

{urlError}

+ )} + {status === 'loading' && (

Shortening your URL...

)} diff --git a/frontend/src/utils/url.ts b/frontend/src/utils/url.ts new file mode 100644 index 0000000..2dc8a0e --- /dev/null +++ b/frontend/src/utils/url.ts @@ -0,0 +1,87 @@ +export function normalizeUrl(input: string) { + if (!/^https?:\/\//i.test(input)) { + return `http://${input}`; + } + return input; +} + +export function isValidUrlForShortening(input: string): boolean { + try { + // Normalize protocol + const hasProtocol = /^https?:\/\//i.test(input); + const inputUrl = hasProtocol ? input : `http://${input}`; + + const parsed = new URL(inputUrl); + + // 1. Protocol must be http or https + if (hasProtocol && !['http:', 'https:'].includes(parsed.protocol)) { + return false; + } + + // 2. Port (if present) must be valid + const port = parsed.port ? parseInt(parsed.port, 10) : null; + if (port !== null && (port < 1 || port > 65535)) { + return false; + } + + // 3. Hostname must exist and be valid + const host = parsed.hostname; + if (!host || host.toLowerCase() === 'localhost') { + return false; + } + + // ❌ Reject trailing dot (e.g., example.com.) + if (inputUrl.includes(`${host}.`)) { + return false; + } + + // ❌ Reject double dots (e.g., example..com) + if (host.includes('..')) { + return false; + } + + // ❌ Reject domains with leading or trailing hyphens + const domainParts = host.split('.'); + if ( + domainParts.length < 2 || + domainParts.some( + part => part.startsWith('-') || part.endsWith('-') || part.length === 0 + ) + ) { + return false; + } + + // ✅ Accept valid IPv4 address + const ipMatch = host.match(/^(\d{1,3}\.){3}\d{1,3}$/); + if (ipMatch) { + const octets = host.split('.').map(Number); + if (octets.length === 4 && octets.every(n => n >= 0 && n <= 255)) { + return true; + } + return false; + } + + // Extract the authority section safely + const authoritySection = (() => { + const parts = input.split('//'); + if (parts.length < 2) return ''; + const afterScheme = parts[1]; + return afterScheme.split('/')[0]; // stops before path/query + })(); + + // Count @ signs in authority only + const atCount = (authoritySection.match(/@/g) || []).length; + if (atCount > 1) { + return false; + } + + // 5. Length limit + if (inputUrl.length > 2048) { + return false; + } + + return true; + } catch (err) { + return false; + } +}