@mikkelscheike/email-provider-links
Version:
TypeScript library for email provider detection with 93 providers (207 domains), concurrent DNS resolution, optimized performance, 94.65% test coverage, and enterprise security for login and password reset flows
214 lines • 7.02 kB
JavaScript
;
/**
* URL Security Validation Module
*
* Provides validation and allowlisting for email provider URLs to prevent
* malicious redirects and supply chain attacks.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateEmailProviderUrl = validateEmailProviderUrl;
exports.validateAllProviderUrls = validateAllProviderUrls;
exports.auditProviderSecurity = auditProviderSecurity;
const loader_1 = require("./loader");
/**
* Get allowlisted domains from provider data
* Only URLs from these domains will be considered safe.
*/
function getAllowedDomains() {
const { providers } = (0, loader_1.loadProviders)();
const allowedDomains = new Set();
for (const provider of providers) {
if (provider.loginUrl) {
try {
const url = new URL(provider.loginUrl);
allowedDomains.add(url.hostname);
}
catch {
// Skip invalid URLs
continue;
}
}
}
return allowedDomains;
}
/**
* Suspicious URL patterns that should always be rejected
*/
const SUSPICIOUS_PATTERNS = [
/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/, // IP addresses
/localhost/i,
/127\.0\.0\.1/,
/192\.168\./,
/10\./,
/172\./,
/\.tk$|\.ml$|\.ga$|\.cf$/i, // Suspicious TLDs
/[a-z0-9]+-[a-z0-9]+-[a-z0-9]+\./i, // Random subdomain patterns
];
/**
* URL shortener domains (should be rejected for security)
*/
const URL_SHORTENERS = [
'bit.ly',
'tinyurl.com',
't.co',
'short.link',
'ow.ly',
'is.gd',
'buff.ly'
];
const idn_1 = require("./idn");
/**
* Validates if a URL is safe for email provider redirects
*
* @param url - The URL to validate
* @returns Validation result with details
*/
function validateEmailProviderUrl(url) {
try {
// Check for malicious patterns in raw URL before parsing
const rawUrl = url.toLowerCase();
// Decode URL to catch encoded malicious patterns
let decodedUrl = '';
try {
decodedUrl = decodeURIComponent(rawUrl);
}
catch {
// If URL can't be decoded, treat as suspicious
return {
isValid: false,
reason: 'URL contains potentially malicious content',
domain: 'unknown'
};
}
// Check both raw and decoded URLs for malicious patterns
const urlsToCheck = [rawUrl, decodedUrl];
for (const urlToCheck of urlsToCheck) {
if (urlToCheck.includes('..') ||
urlToCheck.includes('%2e%2e') ||
urlToCheck.includes('javascript:') ||
urlToCheck.includes('data:') ||
urlToCheck.includes('vbscript:') ||
urlToCheck.includes('file:') ||
urlToCheck.includes('about:') ||
urlToCheck.includes('<script') ||
urlToCheck.includes('onload=') ||
urlToCheck.includes('onerror=')) {
return {
isValid: false,
reason: 'URL contains potentially malicious content',
domain: 'unknown'
};
}
}
// Parse and normalize the URL
const urlObj = new URL(url);
const domain = (0, idn_1.domainToPunycode)(urlObj.hostname.toLowerCase());
const normalizedUrl = urlObj.toString();
// Must use HTTPS
if (urlObj.protocol !== 'https:') {
return {
isValid: false,
reason: 'URL must use HTTPS protocol',
domain
};
}
// Check for suspicious patterns
for (const pattern of SUSPICIOUS_PATTERNS) {
if (pattern.test(domain)) {
return {
isValid: false,
reason: 'URL contains suspicious patterns',
domain
};
}
}
// Check for URL shorteners
if (URL_SHORTENERS.includes(domain)) {
return {
isValid: false,
reason: 'URL shorteners are not allowed',
domain
};
}
// Check if the domain is allowed
const allowedDomains = getAllowedDomains();
if (!allowedDomains.has(domain)) {
return {
isValid: false,
reason: `Domain '${domain}' is not in the allowlist`,
domain
};
}
// Additional security checks for malicious content
const fullUrl = urlObj.toString().toLowerCase();
const pathname = urlObj.pathname.toLowerCase();
const search = urlObj.search.toLowerCase();
// Check for path traversal
if (pathname.includes('..') || pathname.includes('%2e%2e')) {
return {
isValid: false,
reason: 'URL contains potentially malicious content',
domain
};
}
// Check for JavaScript injection
if (fullUrl.includes('javascript:') || search.includes('javascript') || fullUrl.includes('data:')) {
return {
isValid: false,
reason: 'URL contains potentially malicious content',
domain
};
}
return {
isValid: true,
domain,
normalizedUrl
};
}
catch (error) {
return {
isValid: false,
reason: `Invalid URL format: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Validates all URLs in an email providers array
*
* @param providers - Array of email providers to validate
* @returns Array of validation results
*/
function validateAllProviderUrls(providers) {
const results = [];
for (const provider of providers) {
if (provider.loginUrl) {
results.push({
provider: provider.companyProvider || 'Unknown',
url: provider.loginUrl,
validation: validateEmailProviderUrl(provider.loginUrl)
});
}
}
return results;
}
/**
* Security audit function to check all provider URLs
*
* @param providers - Array of email providers to audit
* @returns Security audit report
*/
function auditProviderSecurity(providers) {
const validations = validateAllProviderUrls(providers);
const invalid = validations.filter(v => !v.validation.isValid);
const valid = validations.filter(v => v.validation.isValid);
return {
total: validations.length,
valid: valid.length,
invalid: invalid.length,
invalidProviders: invalid,
report: invalid.length === 0
? '✅ All provider URLs passed security validation'
: `⚠️ ${invalid.length} provider(s) failed security validation`
};
}
//# sourceMappingURL=url-validator.js.map