AngularJS Security and Templates
Keep untrusted content as data rather than compiling it as an AngularJS template. AngularJS is unsupported, and its historical expression restrictions should not be treated as a security sandbox.
How it works
The controlled fixture contrasts plain text with permitted HTML formatting through ngSanitize. The string contains an unsafe event attribute so the review can inspect its removal. This demonstrates a narrow data-handling contract, not a claim that an old framework is safe for a new production system. Strict contextual escaping and HTML sanitization address different contexts and can both be bypassed by an unjustified trust call.
Run the code
AngularJS 1.8.3 is a legacy framework whose official support ended in January 2022. These examples teach maintenance, not a recommendation for a new application. They need a separate local browser page; Bhylu has no AngularJS playground.
In an empty folder save the HTML below as security.html and the server as lab-server.mjs. Make a vendor folder and save the four pinned libraries there, retaining their MIT notices. With Node 22 or later run node lab-server.mjs security.html, then open http://127.0.0.1:8136/. Stop that lab before running a different lesson. The server supplies fixed read-only HTTP fixtures; no data is saved and it does not connect to SQL. No PHP, Apache or MySQL configuration changes are needed.
Example
<!doctype html>
<html lang="en" ng-app="repairLab" ng-strict-di>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>AngularJS Security and Templates</title>
<style>
:root {
font:
18px/1.55 system-ui,
sans-serif;
color: #0f3464;
background: #fff;
color-scheme: light;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
main {
max-width: 50rem;
margin: auto;
padding: 1.2rem;
}
h1 {
font-size: 1.65rem;
}
label {
display: block;
margin: 0.6rem 0;
}
input,
select,
textarea,
button {
font: inherit;
max-width: 100%;
padding: 0.35rem;
margin: 0.2rem;
}
button {
border: 1px solid #034fa0;
border-radius: 0.25rem;
background: #034fa0;
color: white;
}
button:disabled {
opacity: 0.55;
}
output,
[role="status"] {
display: block;
min-height: 1.5rem;
}
table {
border-collapse: collapse;
width: 100%;
}
td,
th {
padding: 0.4rem;
border: 1px solid #a5bbd4;
text-align: left;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
:focus-visible {
outline: 3px solid #0078d4;
outline-offset: 3px;
}
[ng-cloak] {
display: none !important;
}
.marked {
font-weight: 700;
}
.error {
color: #a02424;
}
:root[data-theme="dark"] {
color: #d6e7fb;
background: #061d30;
color-scheme: dark;
}
:root[data-theme="dark"] .error {
color: #ffb4b4;
}
</style>
</head>
<body>
<main ng-controller="DeskController as vm" ng-cloak>
<h1>Community repair desk</h1>
<p>AngularJS 1.8.3 · Legacy maintenance lab · Local demonstration</p>
<p data-result ng-bind="vm.content"></p>
<div class="preview" ng-bind-html="vm.content"></div>
</main>
<script src="/vendor/angular.min.js"></script>
<script src="/vendor/angular-sanitize.min.js"></script>
<script>
(function () {
"use strict";
var app = angular.module("repairLab", ["ngSanitize"]);
app.controller("DeskController", [
function () {
var vm = this;
vm.content =
'<b>Check repair</b><img src="data:," onerror="globalThis.__unsafeRepair=1">';
},
]);
})();
</script>
</body>
</html>
Focus on the bindings and controller
This excerpt leaves out the document shell, styles and library files. Use the complete download to run it.
<p data-result ng-bind="vm.content"></p>
<div class="preview" ng-bind-html="vm.content"></div>
<script>
(function () {
"use strict";
var app = angular.module("repairLab", ["ngSanitize"]);
app.controller("DeskController", [
function () {
var vm = this;
vm.content =
'<b>Check repair</b><img src="data:," onerror="globalThis.__unsafeRepair=1">';
},
]);
})();
</script>
Expected result
<b>Check repair</b><img src="data:," onerror="globalThis.__unsafeRepair=1">
The text paragraph shows the complete string, while the permitted HTML view has no onerror attribute and does not set the fixture marker. A real system also needs a supported maintenance plan, server authorization and a suitable content policy. Do not render visitor text as an AngularJS include or compile it with $compile.