diff --git a/README.md b/README.md index a6ea5db..9f8c48b 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,19 @@ Les traductions sont centralisées dans `src/utils/translations.ts`. Pour ajoute 6. **etsidemain.com** - Site vitrine pour conseil en transformation régénérative 7. **avopieces.fr** - Plateforme juridique IA pour procédures de divorce -## 📱 Responsive Design +## �️ Sécurité + +Ce portfolio implémente des pratiques de sécurité avancées pour protéger contre les vulnérabilités web courantes : + +- **Content Security Policy (CSP)** - Protection contre XSS +- **HSTS** - Forçage HTTPS avec preload +- **COOP/CORP/COEP** - Isolation cross-origin +- **X-Frame-Options** - Protection contre le clickjacking +- **Permissions Policy** - Contrôle des fonctionnalités du navigateur + +Pour plus de détails, consultez [SECURITY.md](./SECURITY.md). + +## �📱 Responsive Design Le portfolio est entièrement responsive avec des breakpoints optimisés : - Mobile : < 640px diff --git a/nginx.conf b/nginx.conf index f8147e0..6ee873a 100644 --- a/nginx.conf +++ b/nginx.conf @@ -6,14 +6,36 @@ server { index index.html; # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; - # Performance headers + # Content Security Policy (CSP) - Protection contre XSS + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; require-trusted-types-for 'script';" always; + + # HTTP Strict Transport Security (HSTS) - Force HTTPS + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + + # Cross-Origin-Opener-Policy (COOP) - Isolation de l'origine + add_header Cross-Origin-Opener-Policy "same-origin" always; + + # Cross-Origin-Resource-Policy (CORP) + add_header Cross-Origin-Resource-Policy "same-origin" always; + + # Cross-Origin-Embedder-Policy (COEP) + add_header Cross-Origin-Embedder-Policy "require-corp" always; + + # Protection contre le clickjacking + add_header X-Frame-Options "DENY" always; + + # Protection contre le MIME type sniffing add_header X-Content-Type-Options "nosniff" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Protection XSS (legacy) + add_header X-XSS-Protection "1; mode=block" always; + + # Referrer Policy - Contrôle des informations envoyées + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Permissions Policy - Contrôle des fonctionnalités du navigateur + add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" always; # Gzip compression gzip on; diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..6353510 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,85 @@ +# Apache Configuration for Security Headers + + + # Content Security Policy (CSP) - Protection contre XSS + Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" + + # HTTP Strict Transport Security (HSTS) + Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" + + # Cross-Origin-Opener-Policy (COOP) + Header always set Cross-Origin-Opener-Policy "same-origin" + + # Cross-Origin-Resource-Policy (CORP) + Header always set Cross-Origin-Resource-Policy "same-origin" + + # Cross-Origin-Embedder-Policy (COEP) + Header always set Cross-Origin-Embedder-Policy "require-corp" + + # Protection contre le clickjacking + Header always set X-Frame-Options "DENY" + + # Protection contre le MIME type sniffing + Header always set X-Content-Type-Options "nosniff" + + # Protection XSS (legacy) + Header always set X-XSS-Protection "1; mode=block" + + # Referrer Policy + Header always set Referrer-Policy "strict-origin-when-cross-origin" + + # Permissions Policy + Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" + + +# Redirection HTTPS + + RewriteEngine On + RewriteCond %{HTTPS} off + RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + + +# SPA Fallback - Toutes les routes vers index.html + + RewriteEngine On + RewriteBase / + RewriteRule ^index\.html$ - [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . /index.html [L] + + +# Cache Control pour les assets statiques + + ExpiresActive On + + # Images + ExpiresByType image/jpeg "access plus 1 year" + ExpiresByType image/jpg "access plus 1 year" + ExpiresByType image/png "access plus 1 year" + ExpiresByType image/gif "access plus 1 year" + ExpiresByType image/webp "access plus 1 year" + ExpiresByType image/svg+xml "access plus 1 year" + ExpiresByType image/x-icon "access plus 1 year" + + # CSS et JavaScript + ExpiresByType text/css "access plus 1 year" + ExpiresByType application/javascript "access plus 1 year" + ExpiresByType text/javascript "access plus 1 year" + + # Fonts + ExpiresByType font/woff "access plus 1 year" + ExpiresByType font/woff2 "access plus 1 year" + ExpiresByType font/ttf "access plus 1 year" + ExpiresByType font/otf "access plus 1 year" + ExpiresByType application/font-woff "access plus 1 year" + ExpiresByType application/font-woff2 "access plus 1 year" + + # HTML (no cache) + ExpiresByType text/html "access plus 0 seconds" + + +# Compression Gzip + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/xml application/rss+xml application/atom+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml + diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..92a8d26 --- /dev/null +++ b/public/_headers @@ -0,0 +1,61 @@ +/* + # Security Headers + + # Content Security Policy (CSP) - Protection contre XSS + Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self' + + # HTTP Strict Transport Security (HSTS) - Force HTTPS + Strict-Transport-Security: max-age=63072000; includeSubDomains; preload + + # Cross-Origin-Opener-Policy (COOP) - Isolation de l'origine + Cross-Origin-Opener-Policy: same-origin + + # Cross-Origin-Resource-Policy (CORP) + Cross-Origin-Resource-Policy: same-origin + + # Cross-Origin-Embedder-Policy (COEP) + Cross-Origin-Embedder-Policy: require-corp + + # Protection contre le clickjacking + X-Frame-Options: DENY + + # Protection contre le MIME type sniffing + X-Content-Type-Options: nosniff + + # Protection XSS (legacy) + X-XSS-Protection: 1; mode=block + + # Referrer Policy - Contrôle des informations envoyées + Referrer-Policy: strict-origin-when-cross-origin + + # Permissions Policy - Contrôle des fonctionnalités du navigateur + Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=() + + # Cache Control + Cache-Control: public, max-age=0, must-revalidate + +/assets/* + # Cache les assets statiques pendant 1 an + Cache-Control: public, max-age=31536000, immutable + +/images/* + # Cache les images pendant 1 an + Cache-Control: public, max-age=31536000, immutable + +/*.js + # Cache les fichiers JS pendant 1 an + Cache-Control: public, max-age=31536000, immutable + +/*.css + # Cache les fichiers CSS pendant 1 an + Cache-Control: public, max-age=31536000, immutable + +/*.woff + # Cache les polices pendant 1 an + Cache-Control: public, max-age=31536000, immutable + Access-Control-Allow-Origin: * + +/*.woff2 + # Cache les polices pendant 1 an + Cache-Control: public, max-age=31536000, immutable + Access-Control-Allow-Origin: * diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1605689..67df977 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -128,7 +128,7 @@ export const Header = () => { animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.4 }} > - @@ -80,6 +81,7 @@ export const ImageGallery = ({ images, projectTitle }: ImageGalleryProps) => { e.stopPropagation(); prevImage(); }} + aria-label="Image précédente" > @@ -103,6 +105,7 @@ export const ImageGallery = ({ images, projectTitle }: ImageGalleryProps) => { e.stopPropagation(); nextImage(); }} + aria-label="Image suivante" > diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..244d9dc --- /dev/null +++ b/vercel.json @@ -0,0 +1,49 @@ +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Content-Security-Policy", + "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://www.google-analytics.com https://www.googletagmanager.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" + }, + { + "key": "Strict-Transport-Security", + "value": "max-age=63072000; includeSubDomains; preload" + }, + { + "key": "Cross-Origin-Opener-Policy", + "value": "same-origin" + }, + { + "key": "Cross-Origin-Resource-Policy", + "value": "same-origin" + }, + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-corp" + }, + { + "key": "X-Frame-Options", + "value": "DENY" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "X-XSS-Protection", + "value": "1; mode=block" + }, + { + "key": "Referrer-Policy", + "value": "strict-origin-when-cross-origin" + }, + { + "key": "Permissions-Policy", + "value": "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" + } + ] + } + ] +}