commit ec2cec76369ddf836a59afa0ba6fb2e8af1f9fd4 Author: Zhongwei Li Date: Sat Nov 29 18:25:02 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..cfa933a --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "angular-security", + "description": "Security best practices with XSS prevention, authentication, and OWASP compliance", + "version": "1.0.0", + "author": { + "name": "Ihsan - Full-Stack Developer & AI Strategist", + "url": "https://github.com/EhssanAtassi" + }, + "skills": [ + "./skills/xss-prevention/SKILL.md", + "./skills/auth-patterns/SKILL.md" + ], + "agents": [ + "./agents/angular-security-expert.md" + ], + "commands": [ + "./commands/secure-component.md", + "./commands/audit-security.md" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f23f82 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# angular-security + +Security best practices with XSS prevention, authentication, and OWASP compliance diff --git a/agents/angular-security-expert.md b/agents/angular-security-expert.md new file mode 100644 index 0000000..340c569 --- /dev/null +++ b/agents/angular-security-expert.md @@ -0,0 +1,110 @@ +# Angular Security Expert + +Expert in Angular application security, XSS prevention, authentication, and OWASP best practices. + +## Expertise + +- **XSS Prevention**: DomSanitizer, Content Security Policy, template security +- **Authentication**: JWT, OAuth2, session management, token storage +- **Authorization**: Route guards, role-based access control (RBAC) +- **CSRF Protection**: Token validation, SameSite cookies +- **Secure Communication**: HTTPS, HTTP interceptors, secure headers +- **Input Validation**: Form validation, sanitization, encoding +- **Dependency Security**: npm audit, vulnerability scanning +- **OWASP Top 10**: Injection, broken auth, sensitive data exposure + +## Core Responsibilities + +1. **Identify security vulnerabilities** in Angular applications +2. **Implement XSS prevention** using DomSanitizer and CSP +3. **Secure authentication flows** with JWT and OAuth2 +4. **Configure authorization** with guards and RBAC +5. **Prevent CSRF attacks** with proper token handling +6. **Validate and sanitize inputs** in forms and APIs +7. **Audit dependencies** for known vulnerabilities +8. **Apply security headers** and HTTPS enforcement + +## Available Commands + +- `/angular-security:secure-component` - Secure component against XSS and injection +- `/angular-security:audit-security` - Comprehensive security audit + +## Available Skills + +- `xss-prevention` - Prevent cross-site scripting attacks +- `auth-patterns` - Secure authentication and authorization patterns + +## Security Philosophy + +- **Defense in depth** - Multiple layers of security +- **Least privilege** - Minimal permissions by default +- **Secure by default** - Security built-in, not bolted-on +- **Zero trust** - Verify everything, trust nothing + +## Code Quality Standards + +1. **Always sanitize** user-generated content +2. **Never trust** client-side validation alone +3. **Use HttpOnly cookies** for sensitive tokens +4. **Implement CSP** headers +5. **Validate all inputs** on server side +6. **Encrypt sensitive data** in transit and at rest +7. **Regular security audits** with npm audit + +## Communication Style + +- Identify **specific vulnerabilities** with severity ratings +- Provide **secure code examples** with explanations +- Reference **OWASP guidelines** when applicable +- Explain **attack vectors** and mitigation strategies +- Prioritize fixes by **risk level** (Critical → High → Medium → Low) + +## Example Interaction + +**User**: "Is this component secure?" + +```typescript +@Component({ + template: `
` +}) +export class ContentComponent { + @Input() userContent: string; +} +``` + +**You**: +``` +🚨 CRITICAL VULNERABILITY: XSS Attack Vector + +Issue: Directly binding unsanitized HTML from user input +Risk: Attackers can inject malicious scripts +CVSS Score: 9.3 (Critical) + +Attack Example: +userContent = '' + +✅ Secure Implementation: + +@Component({ + template: `
` +}) +export class ContentComponent { + @Input() set userContent(value: string) { + this.safeContent = this.sanitizer.sanitize( + SecurityContext.HTML, + value + ); + } + + safeContent: SafeHtml; + + constructor(private sanitizer: DomSanitizer) {} +} + +Or better - avoid innerHTML entirely: +
{{ userContent }}
+``` + +--- + +*Security first, always! 🔒* diff --git a/commands/audit-security.md b/commands/audit-security.md new file mode 100644 index 0000000..0dab98a --- /dev/null +++ b/commands/audit-security.md @@ -0,0 +1,429 @@ +# Audit Security Command + +Comprehensive security audit for Angular applications following OWASP guidelines. + +## Usage + +```bash +/angular-security:audit-security +``` + +## Audit Categories + +### 1. Dependency Vulnerabilities + +```bash +# Check for vulnerable packages +npm audit + +# Fix automatically +npm audit fix + +# View detailed report +npm audit --json + +# Check specific severity +npm audit --audit-level=high +``` + +### 2. XSS Vulnerabilities + +```typescript +// Scan for dangerous patterns: +// - innerHTML with user data +// - bypassSecurityTrust* methods +// - [style], [href], [src] with user input +// - Dynamic script/style injection + +// ❌ Found issues: +[innerHTML]="userContent" // XSS risk +[src]="untrustedUrl" // URL injection +[style]="userStyle" // Style injection +bypassSecurityTrustHtml(userHtml) // Bypassing protection +``` + +### 3. Authentication & Authorization + +```typescript +// ✅ Secure auth implementation +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const token = inject(AuthService).getToken(); + + if (token) { + req = req.clone({ + setHeaders: { + 'Authorization': `Bearer ${token}`, + 'X-Requested-With': 'XMLHttpRequest' // CSRF protection + } + }); + } + + return next(req).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401) { + inject(Router).navigate(['/login']); + } + return throwError(() => error); + }) + ); +}; + +// ✅ Secure route guard +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isAuthenticated()) { + // Check role-based access + const requiredRole = route.data['role']; + if (requiredRole && !authService.hasRole(requiredRole)) { + return router.parseUrl('/unauthorized'); + } + return true; + } + + return router.parseUrl('/login'); +}; +``` + +### 4. CSRF Protection + +```typescript +// ✅ CSRF token implementation +export class CsrfInterceptor implements HttpInterceptor { + intercept(req: HttpRequest, next: HttpHandler): Observable> { + // Get CSRF token from cookie + const csrfToken = this.getCookie('XSRF-TOKEN'); + + // Add to header for state-changing requests + if (req.method !== 'GET' && req.method !== 'HEAD') { + req = req.clone({ + setHeaders: { + 'X-XSRF-TOKEN': csrfToken + } + }); + } + + return next.handle(req); + } + + private getCookie(name: string): string { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? match[2] : ''; + } +} +``` + +### 5. Token Storage + +```typescript +// ❌ INSECURE: localStorage +localStorage.setItem('token', jwt); // Vulnerable to XSS + +// ✅ SECURE: HttpOnly cookie (server-side) +// Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict + +// ✅ ALTERNATIVE: Memory + refresh token +@Injectable({ providedIn: 'root' }) +export class TokenService { + private accessToken: string | null = null; + + setToken(token: string) { + this.accessToken = token; + // Refresh token in HttpOnly cookie + } + + getToken(): string | null { + return this.accessToken; + } + + clearToken() { + this.accessToken = null; + } +} +``` + +### 6. Input Validation + +```typescript +// ✅ Comprehensive validation +export class SecureFormComponent { + form = this.fb.group({ + username: ['', [ + Validators.required, + Validators.minLength(3), + Validators.maxLength(20), + Validators.pattern(/^[a-zA-Z0-9_]+$/) + ]], + email: ['', [ + Validators.required, + Validators.email + ]], + age: ['', [ + Validators.required, + Validators.min(18), + Validators.max(120) + ]], + website: ['', [ + this.urlValidator() + ]] + }); + + private urlValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) return null; + + try { + const url = new URL(control.value); + // Only allow https + if (url.protocol !== 'https:') { + return { invalidProtocol: true }; + } + return null; + } catch { + return { invalidUrl: true }; + } + }; + } +} +``` + +### 7. Secure HTTP Communication + +```typescript +// ✅ HTTPS enforcement +export class AppComponent implements OnInit { + ngOnInit() { + // Redirect HTTP to HTTPS + if (location.protocol !== 'https:' && + !location.hostname.includes('localhost')) { + location.replace(`https:${location.href.substring(location.protocol.length)}`); + } + } +} + +// ✅ Security headers (server-side config) +// Strict-Transport-Security: max-age=31536000; includeSubDomains +// X-Content-Type-Options: nosniff +// X-Frame-Options: DENY +// X-XSS-Protection: 1; mode=block +// Referrer-Policy: strict-origin-when-cross-origin +``` + +### 8. Content Security Policy + +```html + + +``` + +### 9. Rate Limiting + +```typescript +// ✅ Client-side rate limiting +@Injectable({ providedIn: 'root' }) +export class RateLimitService { + private requests = new Map(); + private readonly WINDOW_MS = 60000; // 1 minute + private readonly MAX_REQUESTS = 10; + + canMakeRequest(key: string): boolean { + const now = Date.now(); + const timestamps = this.requests.get(key) || []; + + // Remove old timestamps + const validTimestamps = timestamps.filter(ts => + now - ts < this.WINDOW_MS + ); + + if (validTimestamps.length >= this.MAX_REQUESTS) { + return false; + } + + validTimestamps.push(now); + this.requests.set(key, validTimestamps); + return true; + } +} +``` + +### 10. Logging & Monitoring + +```typescript +// ✅ Security event logging +@Injectable({ providedIn: 'root' }) +export class SecurityLogger { + logSecurityEvent(event: SecurityEvent) { + const log = { + timestamp: new Date().toISOString(), + event: event.type, + severity: event.severity, + user: this.authService.getCurrentUser()?.id, + details: event.details, + userAgent: navigator.userAgent, + ip: this.getClientIp() + }; + + // Send to security monitoring service + this.http.post('/api/security/log', log).subscribe(); + } + + logFailedLogin(username: string) { + this.logSecurityEvent({ + type: 'FAILED_LOGIN', + severity: 'HIGH', + details: { username } + }); + } + + logXSSAttempt(payload: string) { + this.logSecurityEvent({ + type: 'XSS_ATTEMPT', + severity: 'CRITICAL', + details: { payload } + }); + } +} +``` + +## Audit Report Example + +``` +🔒 Security Audit Report +Generated: 2025-10-21 15:30:00 + +┌─────────────────────────────────────────┐ +│ OWASP Top 10 Compliance │ +├─────────────────────────────────────────┤ +│ A01: Broken Access Control ✅ PASS │ +│ A02: Cryptographic Failures ✅ PASS │ +│ A03: Injection ⚠️ WARN │ +│ A04: Insecure Design ✅ PASS │ +│ A05: Security Misconfiguration ❌ FAIL │ +│ A06: Vulnerable Components ⚠️ WARN │ +│ A07: Auth Failures ✅ PASS │ +│ A08: Software/Data Integrity ✅ PASS │ +│ A09: Logging Failures ⚠️ WARN │ +│ A10: Server-Side Forgery ✅ PASS │ +└─────────────────────────────────────────┘ + +📊 Severity Breakdown: +┌──────────┬───────┬──────────────────┐ +│ Severity │ Count │ Issues │ +├──────────┼───────┼──────────────────┤ +│ 🔴 CRITICAL │ 0 │ │ +│ 🔴 HIGH │ 2 │ XSS, CSP missing │ +│ 🟡 MEDIUM │ 5 │ Various │ +│ 🟢 LOW │ 3 │ Improvements │ +└──────────┴───────┴──────────────────┘ + +🔴 HIGH Priority Issues: + +1. XSS Vulnerability in UserProfileComponent + File: user-profile.component.ts:45 + Code: [innerHTML]="userBio" + Fix: Use DomSanitizer or remove innerHTML + +2. Missing Content-Security-Policy + File: index.html + Impact: No XSS protection layer + Fix: Add CSP meta tag + +🟡 MEDIUM Priority Issues: + +3. Hardcoded API credentials + File: environment.ts:8 + Fix: Use environment variables + +4. No rate limiting on login + File: auth.service.ts:23 + Fix: Implement rate limiting + +5. JWT in localStorage + File: token.service.ts:12 + Fix: Use HttpOnly cookies + +🟢 LOW Priority Issues: + +6. Missing security headers + Fix: Configure server headers + +7. No input sanitization + Fix: Add validation to forms + +8. Console.log in production + Fix: Remove debug logs + +📋 npm audit Results: +┌──────────┬───────┐ +│ Critical │ 0 │ +│ High │ 1 │ +│ Moderate │ 3 │ +│ Low │ 8 │ +└──────────┴───────┘ + +Vulnerable packages: +- lodash@4.17.15 (HIGH) → Update to 4.17.21 +- axios@0.21.1 (MODERATE) → Update to 1.6.0 + +✅ Secure Practices Found: +- HTTPS enforced +- Route guards implemented +- Form validation present +- CSRF tokens in use +- Password hashing (bcrypt) + +🎯 Security Score: 7.2/10 + +📈 After Fixes: 9.5/10 + +⏱️ Estimated Fix Time: 4-6 hours + +🔧 Action Plan: +1. Fix XSS vulnerability (30 min) +2. Add CSP headers (15 min) +3. Move JWT to HttpOnly cookie (1 hour) +4. Update vulnerable dependencies (30 min) +5. Add rate limiting (1 hour) +6. Implement security logging (1 hour) +``` + +## Continuous Security + +```yaml +# .github/workflows/security.yml +name: Security Audit + +on: [push, pull_request] + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: npm audit + run: npm audit --audit-level=moderate + + - name: OWASP Dependency Check + uses: dependency-check/Dependency-Check_Action@main + + - name: Snyk Security Scan + uses: snyk/actions/node@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} +``` + +--- + +*Audit regularly, fix proactively! 🛡️* diff --git a/commands/secure-component.md b/commands/secure-component.md new file mode 100644 index 0000000..c314aec --- /dev/null +++ b/commands/secure-component.md @@ -0,0 +1,352 @@ +# Secure Component Command + +Analyze and secure Angular components against XSS, injection, and other vulnerabilities. + +## Usage + +```bash +/angular-security:secure-component +``` + +## Security Checks + +### 1. XSS Prevention + +```typescript +// ❌ CRITICAL: XSS vulnerability +@Component({ + template: `
` +}) +export class UnsafeComponent { + @Input() userInput: string; +} + +// ✅ SECURE: Sanitized HTML +@Component({ + template: `
` +}) +export class SafeComponent { + @Input() set userInput(value: string) { + this.safeContent = this.sanitizer.sanitize( + SecurityContext.HTML, + value + ); + } + + safeContent: SafeHtml; + + constructor(private sanitizer: DomSanitizer) {} +} + +// ✅ BEST: Avoid innerHTML +@Component({ + template: `
{{ userInput }}
` // Auto-escaped +}) +export class BestComponent { + @Input() userInput: string; +} +``` + +### 2. URL Sanitization + +```typescript +// ❌ DANGEROUS: Arbitrary URL +Click + +// ✅ SECURE: Sanitize URLs +export class LinkComponent { + @Input() set url(value: string) { + this.safeUrl = this.sanitizer.sanitize( + SecurityContext.URL, + value + ); + } + + safeUrl: SafeUrl; + + constructor(private sanitizer: DomSanitizer) {} +} + +// Template +Click +``` + +### 3. Resource URL Validation + +```typescript +// ❌ DANGEROUS: Untrusted resource + + +// ✅ SECURE: Whitelist trusted domains +export class VideoComponent { + @Input() set videoUrl(url: string) { + if (this.isTrustedDomain(url)) { + this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); + } else { + console.error('Untrusted video URL:', url); + this.safeUrl = null; + } + } + + safeUrl: SafeResourceUrl | null; + + private trustedDomains = ['youtube.com', 'vimeo.com']; + + private isTrustedDomain(url: string): boolean { + try { + const domain = new URL(url).hostname; + return this.trustedDomains.some(trusted => + domain.includes(trusted) + ); + } catch { + return false; + } + } + + constructor(private sanitizer: DomSanitizer) {} +} +``` + +### 4. Style Injection + +```typescript +// ❌ DANGEROUS: User-controlled styles +
Content
+ +// ✅ SECURE: Sanitize styles +export class StyledComponent { + @Input() set userStyle(value: string) { + this.safeStyle = this.sanitizer.sanitize( + SecurityContext.STYLE, + value + ); + } + + safeStyle: SafeStyle; + + constructor(private sanitizer: DomSanitizer) {} +} + +// ✅ BETTER: Use class binding +
Content
+``` + +### 5. Dynamic Script Loading + +```typescript +// ❌ NEVER DO THIS +eval(userCode); +new Function(userCode)(); + +// ✅ SECURE: No dynamic code execution +// Use configuration objects instead +export class ConfigurableComponent { + @Input() config: { + enabled: boolean; + options: string[]; + }; + + executeAction() { + // Safe configuration-driven logic + if (this.config.enabled) { + this.config.options.forEach(opt => this.handleOption(opt)); + } + } +} +``` + +### 6. Form Input Validation + +```typescript +// ✅ SECURE: Comprehensive validation +export class UserFormComponent { + userForm = this.fb.group({ + username: ['', [ + Validators.required, + Validators.minLength(3), + Validators.maxLength(20), + Validators.pattern(/^[a-zA-Z0-9_]+$/) // Alphanumeric only + ]], + email: ['', [ + Validators.required, + Validators.email + ]], + bio: ['', [ + Validators.maxLength(500) + ]] + }); + + constructor(private fb: FormBuilder) {} + + onSubmit() { + if (this.userForm.valid) { + // Server-side validation still required! + const data = this.userForm.value; + this.api.createUser(data).subscribe(); + } + } +} +``` + +### 7. File Upload Security + +```typescript +// ✅ SECURE: File validation +export class FileUploadComponent { + private readonly MAX_SIZE = 5 * 1024 * 1024; // 5MB + private readonly ALLOWED_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'application/pdf' + ]; + + onFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + + if (!file) return; + + // Validate file type + if (!this.ALLOWED_TYPES.includes(file.type)) { + alert('Invalid file type'); + return; + } + + // Validate file size + if (file.size > this.MAX_SIZE) { + alert('File too large (max 5MB)'); + return; + } + + // Validate file extension + const ext = file.name.split('.').pop()?.toLowerCase(); + const validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf']; + if (!ext || !validExtensions.includes(ext)) { + alert('Invalid file extension'); + return; + } + + this.uploadFile(file); + } +} +``` + +### 8. Sensitive Data in Templates + +```typescript +// ❌ DANGEROUS: Exposing sensitive data +@Component({ + template: ` +
API Key: {{ apiKey }}
+
Password: {{ password }}
+ ` +}) + +// ✅ SECURE: Never expose sensitive data +@Component({ + template: ` +
Status: {{ isAuthenticated ? 'Connected' : 'Disconnected' }}
+ ` +}) +export class SecureComponent { + isAuthenticated: boolean; + + // Sensitive data only in memory, never in template + private apiKey: string; + private password: string; +} +``` + +### 9. Clickjacking Prevention + +```typescript +// Set X-Frame-Options header (server-side) +// But also check in Angular: + +export class FrameGuard implements OnInit { + ngOnInit() { + // Prevent loading in iframe + if (window.self !== window.top) { + console.error('Clickjacking attempt detected'); + window.top.location = window.self.location; + } + } +} +``` + +### 10. Content Security Policy + +```html + + +``` + +## Security Audit Checklist + +```typescript +// Component security audit +export class AuditedComponent { + // ✅ 1. No innerHTML with user data + // ✅ 2. All URLs sanitized + // ✅ 3. Form inputs validated + // ✅ 4. No eval or Function constructor + // ✅ 5. Sensitive data not in template + // ✅ 6. File uploads validated + // ✅ 7. External resources whitelisted + // ✅ 8. CSP headers configured + // ✅ 9. HTTPS enforced + // ✅ 10. Dependencies audited +} +``` + +## Output Example + +``` +🔒 Security Audit: UserProfileComponent + +⚠️ Issues Found: + +1. 🔴 CRITICAL: XSS Vulnerability (Line 23) + Location: template: `
` + Risk: Arbitrary JavaScript execution + Fix: Use sanitizer or remove innerHTML + +2. 🟡 HIGH: Unvalidated File Upload (Line 45) + Location: onFileUpload(event) + Risk: Malicious file upload + Fix: Add file type and size validation + +3. 🟡 MEDIUM: Hardcoded API Key (Line 12) + Location: apiKey = 'sk_live_123...' + Risk: Credential exposure + Fix: Use environment variables + +✅ Secure Patterns Detected: +- Form validation with Validators +- HTTPS enforced +- HttpOnly cookies for auth token + +📋 Recommendations: +1. Sanitize HTML content +2. Validate file uploads +3. Move API key to environment config +4. Add CSP headers +5. Implement rate limiting for API calls + +🎯 Security Score: 6/10 (Medium Risk) +After fixes: 9/10 (Low Risk) +``` + +--- + +*Secure every component! 🛡️* diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..b6664be --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,61 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:EhssanAtassi/angular-marketplace-developer:plugins/angular-security", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "f3b53fcd2b80be7fe0e1843dd43bd1ec4ca9ac6a", + "treeHash": "e8f96394b2d1dfef13dcf7e28a354d7d65fb5ce03bec7acc974f5335b9a02dfa", + "generatedAt": "2025-11-28T10:10:28.685743Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "angular-security", + "description": "Security best practices with XSS prevention, authentication, and OWASP compliance", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "6c7bf2567bbefb3ca680cdfdd5713ef1f2ccbd8a2c16b8d97d1cac900746f3f3" + }, + { + "path": "agents/angular-security-expert.md", + "sha256": "e4e6c196077f81426895f937ecf5e1d282f1c6cfa3c7b7dc27e203af9c6d8068" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "d19571668756bb0dde98a7e4a0ed476fbda61f6b8501f78e27a8fab747075100" + }, + { + "path": "commands/audit-security.md", + "sha256": "dadfb478688e7dca2d11a638a33e7d7610099e8b2b9087fc1c8cb6559877847b" + }, + { + "path": "commands/secure-component.md", + "sha256": "891e5c4a7d779a9fd6f566a80e478c1c4ab150d35a614772d6f0dfa415458509" + }, + { + "path": "skills/auth-patterns/SKILL.md", + "sha256": "8604ccd24c83007c7484cef2ff46a5b48dc51d3a8f20baaac0a07d3aa87fde11" + }, + { + "path": "skills/xss-prevention/SKILL.md", + "sha256": "6a850bf94a9725e33919c63e21c057890d2029ea81bc3130eb6ff167218bd77a" + } + ], + "dirSha256": "e8f96394b2d1dfef13dcf7e28a354d7d65fb5ce03bec7acc974f5335b9a02dfa" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/auth-patterns/SKILL.md b/skills/auth-patterns/SKILL.md new file mode 100644 index 0000000..f75f5a1 --- /dev/null +++ b/skills/auth-patterns/SKILL.md @@ -0,0 +1,902 @@ +# Authentication & Authorization Patterns + +Complete guide to secure authentication and authorization in Angular applications. + +## Table of Contents + +1. [Authentication Basics](#authentication-basics) +2. [JWT Implementation](#jwt-implementation) +3. [OAuth2 & Social Login](#oauth2--social-login) +4. [Token Management](#token-management) +5. [Route Guards](#route-guards) +6. [Role-Based Access Control](#role-based-access-control) +7. [HTTP Interceptors](#http-interceptors) +8. [Session Management](#session-management) + +--- + +## Authentication Basics + +### Authentication Flow + +``` +1. User enters credentials +2. Frontend sends to backend +3. Backend validates credentials +4. Backend generates JWT token +5. Frontend stores token securely +6. Frontend includes token in API requests +7. Backend validates token on each request +``` + +### Secure Authentication Service + +```typescript +@Injectable({ providedIn: 'root' }) +export class AuthService { + private readonly TOKEN_KEY = 'auth_token'; + private currentUserSubject = new BehaviorSubject(null); + public currentUser$ = this.currentUserSubject.asObservable(); + + constructor( + private http: HttpClient, + private router: Router + ) { + // Initialize user from token on app start + this.loadUserFromToken(); + } + + login(email: string, password: string): Observable { + return this.http.post('/api/auth/login', { + email, + password + }).pipe( + tap(response => { + this.setSession(response); + }), + catchError(error => { + console.error('Login failed:', error); + return throwError(() => error); + }) + ); + } + + logout(): void { + // Clear token + localStorage.removeItem(this.TOKEN_KEY); + + // Clear user state + this.currentUserSubject.next(null); + + // Redirect to login + this.router.navigate(['/login']); + + // Optional: Call backend to invalidate token + this.http.post('/api/auth/logout', {}).subscribe(); + } + + isAuthenticated(): boolean { + const token = this.getToken(); + if (!token) return false; + + // Check if token is expired + return !this.isTokenExpired(token); + } + + getToken(): string | null { + return localStorage.getItem(this.TOKEN_KEY); + } + + private setSession(authResult: AuthResponse): void { + // Store token + localStorage.setItem(this.TOKEN_KEY, authResult.token); + + // Update current user + this.currentUserSubject.next(authResult.user); + } + + private loadUserFromToken(): void { + const token = this.getToken(); + if (token && !this.isTokenExpired(token)) { + // Decode token to get user info + const decoded = this.decodeToken(token); + this.currentUserSubject.next(decoded.user); + } + } + + private isTokenExpired(token: string): boolean { + try { + const decoded = this.decodeToken(token); + const expiryTime = decoded.exp * 1000; // Convert to milliseconds + return Date.now() >= expiryTime; + } catch { + return true; + } + } + + private decodeToken(token: string): any { + try { + const payload = token.split('.')[1]; + return JSON.parse(atob(payload)); + } catch { + return null; + } + } +} +``` + +--- + +## JWT Implementation + +### JWT Structure + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. // Header +eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. // Payload +SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c // Signature +``` + +### JWT Payload + +```typescript +interface JwtPayload { + sub: string; // Subject (user ID) + email: string; // User email + name: string; // User name + role: string; // User role + iat: number; // Issued at + exp: number; // Expiration time +} +``` + +### JWT Service + +```typescript +@Injectable({ providedIn: 'root' }) +export class JwtService { + private readonly SECRET_KEY = 'your-secret-key'; // Server-side only! + + // Decode JWT (client-side) + decode(token: string): any { + try { + const parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('Invalid token format'); + } + + const payload = parts[1]; + const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/')); + return JSON.parse(decoded); + } catch (error) { + console.error('Failed to decode token:', error); + return null; + } + } + + // Check if token is expired + isExpired(token: string): boolean { + const decoded = this.decode(token); + if (!decoded || !decoded.exp) return true; + + const expiryTime = decoded.exp * 1000; + return Date.now() >= expiryTime; + } + + // Get expiry date + getExpiryDate(token: string): Date | null { + const decoded = this.decode(token); + if (!decoded || !decoded.exp) return null; + + return new Date(decoded.exp * 1000); + } + + // Get time until expiry + getTimeUntilExpiry(token: string): number { + const expiryDate = this.getExpiryDate(token); + if (!expiryDate) return 0; + + return expiryDate.getTime() - Date.now(); + } +} +``` + +### Token Refresh + +```typescript +@Injectable({ providedIn: 'root' }) +export class TokenRefreshService { + private refreshInProgress = false; + private refreshSubject = new Subject(); + + constructor( + private http: HttpClient, + private authService: AuthService + ) { + // Auto-refresh before expiry + this.setupAutoRefresh(); + } + + refreshToken(): Observable { + if (this.refreshInProgress) { + // Return existing refresh observable + return this.refreshSubject.pipe( + filter(token => !!token), + take(1) + ); + } + + this.refreshInProgress = true; + + return this.http.post<{ token: string }>('/api/auth/refresh', { + refreshToken: this.authService.getRefreshToken() + }).pipe( + tap(response => { + this.authService.setToken(response.token); + this.refreshInProgress = false; + this.refreshSubject.next(response.token); + }), + catchError(error => { + this.refreshInProgress = false; + this.authService.logout(); + return throwError(() => error); + }) + ); + } + + private setupAutoRefresh(): void { + // Refresh token 5 minutes before expiry + const REFRESH_BEFORE_EXPIRY = 5 * 60 * 1000; // 5 minutes + + interval(60000).pipe( // Check every minute + filter(() => this.authService.isAuthenticated()), + switchMap(() => { + const token = this.authService.getToken(); + if (!token) return of(null); + + const timeUntilExpiry = this.getTimeUntilExpiry(token); + + if (timeUntilExpiry <= REFRESH_BEFORE_EXPIRY) { + return this.refreshToken(); + } + return of(null); + }) + ).subscribe(); + } + + private getTimeUntilExpiry(token: string): number { + const decoded = this.decodeToken(token); + if (!decoded?.exp) return 0; + return (decoded.exp * 1000) - Date.now(); + } +} +``` + +--- + +## OAuth2 & Social Login + +### OAuth2 Flow + +``` +1. User clicks "Login with Google" +2. Redirect to OAuth provider (Google) +3. User authorizes app +4. Provider redirects back with authorization code +5. Exchange code for access token +6. Use token to get user info +7. Create session in your app +``` + +### Social Login Service + +```typescript +@Injectable({ providedIn: 'root' }) +export class SocialAuthService { + constructor( + private http: HttpClient, + private authService: AuthService + ) {} + + // Google OAuth2 + loginWithGoogle(): void { + const clientId = environment.googleClientId; + const redirectUri = `${window.location.origin}/auth/google/callback`; + const scope = 'openid email profile'; + + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` + + `client_id=${clientId}&` + + `redirect_uri=${encodeURIComponent(redirectUri)}&` + + `response_type=code&` + + `scope=${encodeURIComponent(scope)}`; + + window.location.href = authUrl; + } + + // Handle OAuth callback + handleOAuthCallback(code: string, provider: string): Observable { + return this.http.post('/api/auth/oauth/callback', { + code, + provider + }).pipe( + tap(response => { + this.authService.setSession(response); + }) + ); + } + + // GitHub OAuth2 + loginWithGitHub(): void { + const clientId = environment.githubClientId; + const redirectUri = `${window.location.origin}/auth/github/callback`; + + const authUrl = `https://github.com/login/oauth/authorize?` + + `client_id=${clientId}&` + + `redirect_uri=${encodeURIComponent(redirectUri)}&` + + `scope=user:email`; + + window.location.href = authUrl; + } +} +``` + +### OAuth Callback Component + +```typescript +@Component({ + template: `
Completing login...
` +}) +export class OAuthCallbackComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private socialAuth: SocialAuthService, + private router: Router + ) {} + + ngOnInit() { + // Get authorization code from URL + this.route.queryParams.subscribe(params => { + const code = params['code']; + const provider = this.route.snapshot.paramMap.get('provider'); + + if (code && provider) { + this.socialAuth.handleOAuthCallback(code, provider).subscribe({ + next: () => { + this.router.navigate(['/dashboard']); + }, + error: (error) => { + console.error('OAuth callback failed:', error); + this.router.navigate(['/login'], { + queryParams: { error: 'oauth_failed' } + }); + } + }); + } + }); + } +} +``` + +--- + +## Token Management + +### Secure Token Storage + +```typescript +@Injectable({ providedIn: 'root' }) +export class SecureTokenService { + // Option 1: Memory (most secure, lost on refresh) + private token: string | null = null; + + setToken(token: string): void { + this.token = token; + } + + getToken(): string | null { + return this.token; + } + + clearToken(): void { + this.token = null; + } +} + +// Option 2: localStorage (survives refresh, vulnerable to XSS) +class LocalStorageTokenService { + private readonly KEY = 'auth_token'; + + setToken(token: string): void { + localStorage.setItem(this.KEY, token); + } + + getToken(): string | null { + return localStorage.getItem(this.KEY); + } + + clearToken(): void { + localStorage.removeItem(this.KEY); + } +} + +// Option 3: HttpOnly cookie (most secure, set server-side) +// Backend sets: Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict +// Frontend: Token automatically sent with requests +``` + +### Token Encryption (Client-Side) + +```typescript +@Injectable({ providedIn: 'root' }) +export class EncryptedTokenService { + private readonly KEY = 'auth_token'; + private readonly ENCRYPTION_KEY = 'your-encryption-key'; // From environment + + setToken(token: string): void { + const encrypted = this.encrypt(token); + localStorage.setItem(this.KEY, encrypted); + } + + getToken(): string | null { + const encrypted = localStorage.getItem(this.KEY); + if (!encrypted) return null; + + return this.decrypt(encrypted); + } + + private encrypt(text: string): string { + // Use Web Crypto API + // This is a simplified example + return btoa(text); // In production, use proper encryption + } + + private decrypt(encrypted: string): string { + try { + return atob(encrypted); + } catch { + return ''; + } + } +} +``` + +--- + +## Route Guards + +### Auth Guard + +```typescript +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isAuthenticated()) { + return true; + } + + // Redirect to login with return URL + return router.createUrlTree(['/login'], { + queryParams: { returnUrl: state.url } + }); +}; + +// Usage in routes +export const routes: Routes = [ + { + path: 'dashboard', + canActivate: [authGuard], + loadComponent: () => import('./dashboard/dashboard.component') + } +]; +``` + +### Role Guard + +```typescript +export const roleGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + // Check authentication + if (!authService.isAuthenticated()) { + return router.createUrlTree(['/login']); + } + + // Check role + const requiredRole = route.data['role'] as string; + const userRole = authService.getCurrentUser()?.role; + + if (requiredRole && userRole !== requiredRole) { + console.warn(`Access denied. Required role: ${requiredRole}, User role: ${userRole}`); + return router.createUrlTree(['/unauthorized']); + } + + return true; +}; + +// Usage +{ + path: 'admin', + canActivate: [authGuard, roleGuard], + data: { role: 'admin' }, + loadChildren: () => import('./admin/admin.routes') +} +``` + +### Permission Guard + +```typescript +export const permissionGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (!authService.isAuthenticated()) { + return router.createUrlTree(['/login']); + } + + const requiredPermissions = route.data['permissions'] as string[]; + const userPermissions = authService.getCurrentUser()?.permissions || []; + + const hasPermission = requiredPermissions.every(permission => + userPermissions.includes(permission) + ); + + if (!hasPermission) { + return router.createUrlTree(['/forbidden']); + } + + return true; +}; + +// Usage +{ + path: 'users/edit/:id', + canActivate: [authGuard, permissionGuard], + data: { permissions: ['users.edit', 'users.view'] }, + component: UserEditComponent +} +``` + +### Can Deactivate Guard + +```typescript +export interface CanComponentDeactivate { + canDeactivate: () => boolean | Observable; +} + +export const unsavedChangesGuard: CanDeactivateFn = ( + component +) => { + return component.canDeactivate + ? component.canDeactivate() + : true; +}; + +// Component implementation +@Component({}) +export class EditFormComponent implements CanComponentDeactivate { + hasUnsavedChanges = false; + + canDeactivate(): boolean { + if (this.hasUnsavedChanges) { + return confirm('You have unsaved changes. Are you sure you want to leave?'); + } + return true; + } +} + +// Route +{ + path: 'edit/:id', + component: EditFormComponent, + canDeactivate: [unsavedChangesGuard] +} +``` + +--- + +## Role-Based Access Control + +### RBAC Service + +```typescript +interface Permission { + resource: string; + actions: string[]; +} + +interface Role { + name: string; + permissions: Permission[]; +} + +@Injectable({ providedIn: 'root' }) +export class RbacService { + private roles: Map = new Map([ + ['admin', { + name: 'admin', + permissions: [ + { resource: 'users', actions: ['create', 'read', 'update', 'delete'] }, + { resource: 'posts', actions: ['create', 'read', 'update', 'delete'] }, + { resource: 'settings', actions: ['read', 'update'] } + ] + }], + ['editor', { + name: 'editor', + permissions: [ + { resource: 'posts', actions: ['create', 'read', 'update'] }, + { resource: 'media', actions: ['create', 'read', 'delete'] } + ] + }], + ['viewer', { + name: 'viewer', + permissions: [ + { resource: 'posts', actions: ['read'] }, + { resource: 'media', actions: ['read'] } + ] + }] + ]); + + constructor(private authService: AuthService) {} + + hasPermission(resource: string, action: string): boolean { + const user = this.authService.getCurrentUser(); + if (!user || !user.role) return false; + + const role = this.roles.get(user.role); + if (!role) return false; + + const permission = role.permissions.find(p => p.resource === resource); + return permission?.actions.includes(action) || false; + } + + hasRole(roleName: string): boolean { + const user = this.authService.getCurrentUser(); + return user?.role === roleName; + } + + hasAnyRole(roles: string[]): boolean { + const user = this.authService.getCurrentUser(); + return roles.includes(user?.role || ''); + } +} +``` + +### Permission Directive + +```typescript +@Directive({ + selector: '[hasPermission]', + standalone: true +}) +export class HasPermissionDirective implements OnInit { + @Input() hasPermission!: { resource: string; action: string }; + + constructor( + private rbac: RbacService, + private templateRef: TemplateRef, + private viewContainer: ViewContainerRef + ) {} + + ngOnInit() { + const { resource, action } = this.hasPermission; + + if (this.rbac.hasPermission(resource, action)) { + this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); + } + } +} + +// Usage + +``` + +### Role Directive + +```typescript +@Directive({ + selector: '[hasRole]', + standalone: true +}) +export class HasRoleDirective implements OnInit { + @Input() hasRole!: string | string[]; + + constructor( + private rbac: RbacService, + private templateRef: TemplateRef, + private viewContainer: ViewContainerRef + ) {} + + ngOnInit() { + const roles = Array.isArray(this.hasRole) ? this.hasRole : [this.hasRole]; + + if (this.rbac.hasAnyRole(roles)) { + this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); + } + } +} + +// Usage +
+ Admin content +
+ +
+ Admin or Editor content +
+``` + +--- + +## HTTP Interceptors + +### Auth Interceptor + +```typescript +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(AuthService); + const token = authService.getToken(); + + // Skip auth for certain URLs + if (req.url.includes('/auth/login') || req.url.includes('/auth/register')) { + return next(req); + } + + // Add auth token + if (token) { + req = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}`, + 'X-Requested-With': 'XMLHttpRequest' + } + }); + } + + return next(req).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401) { + // Token expired or invalid + authService.logout(); + inject(Router).navigate(['/login']); + } + return throwError(() => error); + }) + ); +}; +``` + +### Token Refresh Interceptor + +```typescript +export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(AuthService); + const tokenService = inject(TokenRefreshService); + + return next(req).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401 && !req.url.includes('/auth/refresh')) { + // Try to refresh token + return tokenService.refreshToken().pipe( + switchMap(newToken => { + // Retry request with new token + const cloned = req.clone({ + setHeaders: { + Authorization: `Bearer ${newToken}` + } + }); + return next(cloned); + }), + catchError(refreshError => { + authService.logout(); + return throwError(() => refreshError); + }) + ); + } + return throwError(() => error); + }) + ); +}; +``` + +--- + +## Session Management + +### Session Service + +```typescript +@Injectable({ providedIn: 'root' }) +export class SessionService { + private readonly TIMEOUT_DURATION = 30 * 60 * 1000; // 30 minutes + private timeoutId: any; + private lastActivity: number = Date.now(); + + constructor( + private authService: AuthService, + private router: Router + ) { + this.startMonitoring(); + } + + private startMonitoring(): void { + // Monitor user activity + fromEvent(document, 'click').pipe( + merge( + fromEvent(document, 'keypress'), + fromEvent(document, 'mousemove'), + fromEvent(document, 'scroll') + ), + throttleTime(1000) + ).subscribe(() => { + this.resetTimeout(); + }); + + this.resetTimeout(); + } + + private resetTimeout(): void { + this.lastActivity = Date.now(); + + // Clear existing timeout + if (this.timeoutId) { + clearTimeout(this.timeoutId); + } + + // Set new timeout + this.timeoutId = setTimeout(() => { + this.handleTimeout(); + }, this.TIMEOUT_DURATION); + } + + private handleTimeout(): void { + console.log('Session timeout - logging out'); + this.authService.logout(); + this.router.navigate(['/login'], { + queryParams: { reason: 'session_timeout' } + }); + } + + getLastActivity(): Date { + return new Date(this.lastActivity); + } + + getRemainingTime(): number { + const elapsed = Date.now() - this.lastActivity; + return Math.max(0, this.TIMEOUT_DURATION - elapsed); + } +} +``` + +--- + +## Best Practices + +✅ **DO**: +- Use HTTPS for all auth endpoints +- Store tokens in HttpOnly cookies when possible +- Implement token refresh +- Use route guards for protected routes +- Validate tokens on server side +- Implement session timeout +- Log security events +- Use strong password policies + +❌ **DON'T**: +- Store sensitive data in localStorage +- Send tokens in URL parameters +- Trust client-side validation alone +- Use weak encryption +- Expose user roles/permissions in JWT +- Skip CSRF protection +- Hardcode secrets in code + +--- + +*Authenticate securely, authorize precisely! 🔐* diff --git a/skills/xss-prevention/SKILL.md b/skills/xss-prevention/SKILL.md new file mode 100644 index 0000000..c920293 --- /dev/null +++ b/skills/xss-prevention/SKILL.md @@ -0,0 +1,731 @@ +# XSS Prevention in Angular + +Complete guide to preventing Cross-Site Scripting (XSS) attacks in Angular applications. + +## Table of Contents + +1. [Understanding XSS](#understanding-xss) +2. [Angular's Built-in Protection](#angulars-built-in-protection) +3. [DomSanitizer](#domsanitizer) +4. [Content Security Policy](#content-security-policy) +5. [Secure Coding Patterns](#secure-coding-patterns) +6. [Common Vulnerabilities](#common-vulnerabilities) +7. [Testing for XSS](#testing-for-xss) + +--- + +## Understanding XSS + +### Types of XSS + +**1. Stored XSS (Persistent)** +```typescript +// Attacker stores malicious script in database +userBio = ''; + +// Later displayed to other users +
// Executes script +``` + +**2. Reflected XSS (Non-persistent)** +```typescript +// Malicious link: https://example.com?search= + +// App reflects input without sanitization +
Search results for: {{ searchQuery }}
+``` + +**3. DOM-based XSS** +```typescript +// URL: https://example.com# + +// Unsafe DOM manipulation +element.innerHTML = location.hash.substring(1); +``` + +### XSS Attack Vectors + +```typescript +// Script tags + + +// Event handlers + +
+ +// Data URLs + + +// JavaScript URLs + + +// Style injection +
+ +// SVG + + +// Object/embed + +``` + +--- + +## Angular's Built-in Protection + +### Automatic Escaping + +```typescript +// ✅ SAFE: Angular auto-escapes +@Component({ + template: ` +
{{ userInput }}
+
+ ` +}) +export class SafeComponent { + userInput = ''; + // Rendered as text, not executed +} +``` + +### Security Contexts + +Angular sanitizes based on context: + +| Context | Element | Sanitization | +|---------|---------|--------------| +| HTML | `[innerHTML]` | Remove scripts, styles | +| Style | `[style]` | Remove dangerous CSS | +| URL | `[href]`, `[src]` | Block javascript: | +| Resource URL | ` + +// ✅ SECURE: Whitelist domains +@Component({ + template: `` +}) +export class VideoComponent { + @Input() set videoUrl(url: string) { + const trusted = ['youtube.com', 'vimeo.com']; + try { + const domain = new URL(url).hostname; + if (trusted.some(t => domain.includes(t))) { + this.trustedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url); + } else { + console.warn('Untrusted video domain:', domain); + this.trustedUrl = null; + } + } catch { + this.trustedUrl = null; + } + } + trustedUrl: SafeResourceUrl | null; + constructor(private sanitizer: DomSanitizer) {} +} +``` + +--- + +## Testing for XSS + +### Manual Testing + +```typescript +// Test payloads +const xssPayloads = [ + '', + '', + '', + 'javascript:alert("XSS")', + '', + '', + '