
Introduction
User input is one of the most common gateways for security vulnerabilities, especially Cross-Site Scripting (XSS) attacks. If malicious code slips into your application, it can execute in your users’ browsers, leading to stolen data, compromised accounts, and damaged trust. While Angular provides some built-in protection against XSS, you should still validate inputs at the form level to catch and block unsafe content early.
In this article, we’ll walk through creating a custom Angular validator that uses regex patterns to detect and reject malicious inputs such as embedded scripts, encoded HTML tags, and event handlers. By integrating this validator into your forms, you can add an extra layer of defense to your application’s security.
Understanding the Malicious Patterns
Our validator works by scanning user input against a set of regular expressions — each designed to catch a different category of potentially dangerous content. Let’s break them down.
Script and media tags
/<script/,
/<img/,
/<(iframe|object|embed|svg|link|style)/,
These patterns look for HTML tags that are notorious for carrying malicious payloads:
<script>
— The classic XSS injection point. If an attacker can insert this, they can run arbitrary JavaScript in the user’s browser.<img>
— Often used withonerror
to trigger JavaScript when the image fails to load.<iframe>
,<object>
,<embed>
,<svg>
,<link>
,<style>
— These tags can load external resources, execute scripts, or inject styles that manipulate the page in unsafe ways.
Event handler attributes
/on\w+=/, // e.g., onload=, onclick=
Any HTML attribute starting with on
can execute JavaScript when a certain event occurs.
For example:
<div onmouseover="alert('XSS')"></div>
This regex catches all of them — onload
, onclick
, onmouseover
, and many more.
JavaScript protocols and data URIs
/javascript:/,
/data:(text\/html|text\/javascript|application\/javascript);base64/,
javascript:
— A URL scheme that executes JavaScript code directly in the browser. Common in malicious<a href>
links.data:
URIs — These can embed Base64-encoded HTML or JavaScript directly inside an element source. For example:
<img src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">
Both allow attackers to smuggle in executable code without needing an external file.
Encoded characters
/%3c/, // encoded <
/%3e/, // encoded >
/&#x[0-9a-f]+;/,
/\\x[0-9a-f]{2}/,
/\\u[0-9a-f]{4}/,
Attackers often try to obfuscate dangerous characters so they slip past filters:
%3c
→ URL-encoded<
%3e
→ URL-encoded>
&#x...;
→ HTML entity encoding (hexadecimal)\x..
→ Hexadecimal escape sequences in JavaScript strings\u....
→ Unicode escape sequences
Even if your HTML filter catches <script>
, these encodings could bypass it — unless you detect them too. By grouping our patterns this way, we cover both obvious attack vectors (like <script>
) and sneaky obfuscation tricks that more sophisticated attackers might use.
Implementing the XSS-Blocking Validator
To protect your Angular forms from Cross-Site Scripting (XSS) attacks, we can create a custom validator that scans user input for suspicious patterns before it’s ever sent to the server.
The core idea is:
- Normalize the input so attackers can’t sneak around with spaces or case variations.
- Match against a set of known dangerous patterns — things like
<script>
tags, event handlers (onload=
,onclick=
), JavaScript URIs, and encoded HTML characters. - Return an error if any match is found so the form can block submission.
Here’s how that looks in code:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export class CustomValidators {
static maliciousContentValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control.value) return null; // Allow empty values
// Step 1: Normalize input
const value = String(control.value).toLowerCase().replace(/\s+/g, '');
// Step 2: Patterns for malicious content
const patterns = [
/<script/,
/<img/,
/<(iframe|object|embed|svg|link|style)/,
/on\w+=/, // event handlers like onload=, onclick=
/javascript:/,
/data:(text\/html|text\/javascript|application\/javascript);base64/,
/%3c/, // encoded <
/%3e/, // encoded >
/&#x[0-9a-f]+;/,
/\\x[0-9a-f]{2}/,
/\\u[0-9a-f]{4}/,
];
// Step 3: Check for matches
const isMalicious = patterns.some((pattern) => pattern.test(value));
// Step 4: Return validation result
return isMalicious ? { maliciousContent: true } : null;
};
}
}
Add it to a form control
Once the validator is defined, you can plug it into any Angular FormControl
alongside built-in validators. This way, your field will automatically block unsafe input during normal form validation.
this.form = this.fb.group({
title: [
'',
[
Validators.required,
Validators.maxLength(40),
CustomValidators.maliciousContentValidator(),
],
],
});
Using it in the HTML
After attaching the validator to your form control, you can show a helpful error message to the user when their input contains suspicious content. We check the control’s errors
object and only display the message after the field has been touched to avoid flashing errors too early.
<input
formControlName="title"
placeholder="Enter title"
/>
@if (form.get('title')?.hasError('maliciousContent') && form.get('title')?.touched) {
<div class="error">
Your input looks unsafe (possible XSS). Please remove script-like content.
</div>
}
This ensures that users get immediate feedback if they accidentally (or intentionally) enter something that resembles an XSS payload.
Conclusion
While Angular already includes built-in XSS protections, validating user input at the form level adds another important layer of defense. By implementing a custom maliciousContentValidator
, you can catch and block potentially dangerous patterns before they ever reach your server. This approach not only strengthens security against XSS attacks but also improves the user experience by providing immediate, clear feedback when unsafe content is detected.
Remember — client-side validation should always be paired with robust server-side checks and proper output encoding. Security works best when it’s defense in depth.