From 506004db17d4b2219c2f10bc60a50405464dcd94 Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Tue, 16 Dec 2025 13:19:36 +0530 Subject: [PATCH] fix(security): prevent XSS attacks and URL injection vulnerabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical security improvements to eliminate XSS and CSP bypass vulnerabilities. Changes: 1. Added validator.js utility with URL and email validation - isValidUrl(): Validates URLs against allowed protocols (http/https/mailto) - sanitizeUrl(): Returns safe URL or '#' fallback - isValidEmail(): Basic email format validation 2. Fixed inline event handler in experience.js (CSP bypass) - Removed onerror="this.remove()" inline handler - Replaced with addEventListener for proper error handling - Prevents Content Security Policy bypass 3. Applied URL sanitization to projects.js - Sanitized project.link before href assignment - Sanitized project.github before href assignment - Prevents javascript: and data: URL injection Security Impact: - Eliminates inline event handler CSP bypass vulnerability - Prevents javascript: URL injection attacks - Validates all external URLs before use in href attributes - Adds defense-in-depth for URL-based XSS vectors Testing: - Malicious URLs (javascript:alert(1)) now sanitized to '#' - Only http/https/mailto protocols allowed - Relative paths (/, #) still supported 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/assets/js/components/experience.js | 42 +++++++++++++++----- src/assets/js/components/projects.js | 14 ++++--- src/assets/js/utils/validator.js | 53 ++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 src/assets/js/utils/validator.js diff --git a/src/assets/js/components/experience.js b/src/assets/js/components/experience.js index 78d241e..088d952 100644 --- a/src/assets/js/components/experience.js +++ b/src/assets/js/components/experience.js @@ -2,6 +2,8 @@ * Experience section component */ +import { sanitizeUrl } from '../utils/validator.js'; + export const populateExperience = (experience) => { const experienceContainer = document.getElementById('experience-container'); if (!experienceContainer) return; @@ -11,21 +13,16 @@ export const populateExperience = (experience) => { // The fallback icon now acts as a base layer. const fallbackIconHTML = `
`; - // The image is an overlay that will be removed on error. - const imageHTML = ` - `; + // The image will be added via JavaScript to handle errors properly + const imageContainerHTML = exp.logoUrl + ? `
` + : ''; return `
${fallbackIconHTML} - ${exp.logoUrl ? imageHTML : ''} + ${imageContainerHTML}

${exp.title}

@@ -41,4 +38,29 @@ export const populateExperience = (experience) => { .join(''); experienceContainer.innerHTML = experienceHTML; + + // Add company logos with proper error handling (no inline handlers) + const logoContainers = experienceContainer.querySelectorAll('.company-logo-container'); + logoContainers.forEach((container) => { + const logoUrl = container.dataset.logoUrl; + const company = container.dataset.company; + + if (logoUrl && logoUrl !== '#') { + const img = document.createElement('img'); + img.src = logoUrl; + img.alt = `${company} Logo`; + img.className = + 'company-logo w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-700'; + img.style.position = 'absolute'; + img.style.top = '0'; + img.style.left = '0'; + + // Use addEventListener instead of inline onerror + img.addEventListener('error', () => { + img.remove(); + }); + + container.appendChild(img); + } + }); }; diff --git a/src/assets/js/components/projects.js b/src/assets/js/components/projects.js index 95be364..758e87f 100644 --- a/src/assets/js/components/projects.js +++ b/src/assets/js/components/projects.js @@ -2,6 +2,8 @@ * Projects section with modern design */ +import { sanitizeUrl } from '../utils/validator.js'; + export const populateProjects = (projects) => { const projectsContainer = document.getElementById('projects-container'); if (!projectsContainer) return; @@ -65,10 +67,10 @@ export const populateProjects = (projects) => { ${ project.link ? ` - @@ -81,8 +83,8 @@ export const populateProjects = (projects) => { ${ project.github ? ` - { + if (!url || typeof url !== 'string') { + return false; + } + + try { + const parsed = new URL(url, window.location.origin); + // Only allow http, https, and mailto protocols + return ['http:', 'https:', 'mailto:'].includes(parsed.protocol); + } catch { + // If URL parsing fails, treat as relative URL + // Check if it starts with / (relative path) or # (anchor) + return url.startsWith('/') || url.startsWith('#'); + } +}; + +/** + * Sanitizes a URL to prevent XSS attacks + * Returns a safe URL or fallback to '#' + * + * @param {string} url - The URL to sanitize + * @returns {string} - Safe URL or '#' if invalid + */ +export const sanitizeUrl = (url) => { + return isValidUrl(url) ? url : '#'; +}; + +/** + * Validates email address format + * + * @param {string} email - The email to validate + * @returns {boolean} - True if email format is valid + */ +export const isValidEmail = (email) => { + if (!email || typeof email !== 'string') { + return false; + } + + // Basic email validation regex + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +};