|
|
@@ -2,6 +2,133 @@
|
|
|
// Comprehensive URL validation for video platforms
|
|
|
|
|
|
class URLValidator {
|
|
|
+ /**
|
|
|
+ * Detect punycode/IDN domains that might be used for spoofing
|
|
|
+ * @param {string} hostname - Domain hostname to check
|
|
|
+ * @returns {boolean} True if punycode detected
|
|
|
+ */
|
|
|
+ static isPunycode(hostname) {
|
|
|
+ if (!hostname || typeof hostname !== 'string') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Punycode domains start with "xn--"
|
|
|
+ return hostname.toLowerCase().includes('xn--');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Detect potential homograph attacks using lookalike characters
|
|
|
+ * @param {string} hostname - Domain hostname to check
|
|
|
+ * @returns {object} Detection result with suspicious characters
|
|
|
+ */
|
|
|
+ static detectHomographAttack(hostname) {
|
|
|
+ if (!hostname || typeof hostname !== 'string') {
|
|
|
+ return { suspicious: false, characters: [] };
|
|
|
+ }
|
|
|
+
|
|
|
+ // Common homograph character mappings
|
|
|
+ const suspiciousChars = [
|
|
|
+ // Cyrillic lookalikes
|
|
|
+ { char: 'а', lookalike: 'a', name: 'Cyrillic а' },
|
|
|
+ { char: 'е', lookalike: 'e', name: 'Cyrillic е' },
|
|
|
+ { char: 'о', lookalike: 'o', name: 'Cyrillic о' },
|
|
|
+ { char: 'р', lookalike: 'p', name: 'Cyrillic р' },
|
|
|
+ { char: 'с', lookalike: 'c', name: 'Cyrillic с' },
|
|
|
+ { char: 'у', lookalike: 'y', name: 'Cyrillic у' },
|
|
|
+ { char: 'х', lookalike: 'x', name: 'Cyrillic х' },
|
|
|
+
|
|
|
+ // Greek lookalikes
|
|
|
+ { char: 'ο', lookalike: 'o', name: 'Greek omicron' },
|
|
|
+ { char: 'ν', lookalike: 'v', name: 'Greek nu' },
|
|
|
+ { char: 'α', lookalike: 'a', name: 'Greek alpha' },
|
|
|
+
|
|
|
+ // Other suspicious characters
|
|
|
+ { char: 'і', lookalike: 'i', name: 'Ukrainian i' },
|
|
|
+ { char: 'ј', lookalike: 'j', name: 'Cyrillic j' }
|
|
|
+ ];
|
|
|
+
|
|
|
+ const found = [];
|
|
|
+ const lowerHostname = hostname.toLowerCase();
|
|
|
+
|
|
|
+ for (const { char, lookalike, name } of suspiciousChars) {
|
|
|
+ if (lowerHostname.includes(char)) {
|
|
|
+ found.push({ char, lookalike, name });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check for mixed scripts (ASCII + non-ASCII)
|
|
|
+ const hasAscii = /[a-z]/i.test(hostname);
|
|
|
+ const hasNonAscii = /[^\x00-\x7F]/.test(hostname);
|
|
|
+ const mixedScripts = hasAscii && hasNonAscii;
|
|
|
+
|
|
|
+ return {
|
|
|
+ suspicious: found.length > 0 || mixedScripts,
|
|
|
+ characters: found,
|
|
|
+ mixedScripts
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Validate URL against security threats (punycode, homographs)
|
|
|
+ * @param {string} url - URL to validate
|
|
|
+ * @returns {object} Validation result with security warnings
|
|
|
+ */
|
|
|
+ static validateUrlSecurity(url) {
|
|
|
+ if (!url || typeof url !== 'string') {
|
|
|
+ return { safe: false, warnings: ['Invalid URL'] };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const parsed = new URL(url.startsWith('http') ? url : 'https://' + url);
|
|
|
+ const hostname = parsed.hostname.toLowerCase();
|
|
|
+ const warnings = [];
|
|
|
+
|
|
|
+ // Check for punycode
|
|
|
+ if (this.isPunycode(hostname)) {
|
|
|
+ warnings.push('⚠️ Punycode domain detected - may be used for spoofing');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check for homograph attacks
|
|
|
+ const homograph = this.detectHomographAttack(hostname);
|
|
|
+ if (homograph.suspicious) {
|
|
|
+ if (homograph.characters.length > 0) {
|
|
|
+ warnings.push(`⚠️ Suspicious lookalike characters detected: ${homograph.characters.map(c => c.name).join(', ')}`);
|
|
|
+ }
|
|
|
+ if (homograph.mixedScripts) {
|
|
|
+ warnings.push('⚠️ Mixed character scripts detected - potential homograph attack');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Verify against trusted domains
|
|
|
+ const trustedDomains = [
|
|
|
+ 'youtube.com',
|
|
|
+ 'youtu.be',
|
|
|
+ 'vimeo.com'
|
|
|
+ ];
|
|
|
+
|
|
|
+ const isTrusted = trustedDomains.some(domain =>
|
|
|
+ hostname === domain ||
|
|
|
+ hostname.endsWith('.' + domain) ||
|
|
|
+ hostname === 'www.' + domain
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!isTrusted && warnings.length === 0) {
|
|
|
+ warnings.push('⚠️ Domain not in trusted list');
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ safe: warnings.length === 0,
|
|
|
+ warnings,
|
|
|
+ hostname,
|
|
|
+ isTrusted
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return {
|
|
|
+ safe: false,
|
|
|
+ warnings: ['Invalid URL format']
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
// Check if URL is a valid video URL from supported platforms
|
|
|
static isValidVideoUrl(url) {
|
|
|
if (!url || typeof url !== 'string') {
|
|
|
@@ -13,6 +140,16 @@ class URLValidator {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+ // SECURITY: Check for punycode and homograph attacks
|
|
|
+ const securityCheck = this.validateUrlSecurity(trimmedUrl);
|
|
|
+ if (!securityCheck.safe) {
|
|
|
+ // Log security warnings but still allow if domain is trusted
|
|
|
+ if (!securityCheck.isTrusted) {
|
|
|
+ console.warn('URL security warnings:', securityCheck.warnings);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// Check against supported platforms
|
|
|
return this.isYouTubeUrl(trimmedUrl) ||
|
|
|
this.isVimeoUrl(trimmedUrl) ||
|