Get listed

AngularJS security: 1.x is EOL, migrate, then lock SCE

A retired clock with hatches still open in coral.

AngularJS is end of life. Official long-term support ended on 31 December 2021. Every new XSS class in the 1.x line is yours.

If you still ship 1.x, the security story is containment: CSP, a sanitizer you maintain, and a rewrite plan. The default interpolation in 1.x is not a reason to stay.

The usual mistake is a ‘we will migrate next year’ ticket that is now four years old, plus a new feature in the same 1.x app.

This page is what is still true about AngularJS sinks, and why the current answer is leave 1.x, not tune it.

AngularJS official long-term support ended on 31 December 2021. That is four years and eight months of no upstream security fix as of 22 August 2026. Angular blog post dated 1 February 2021. Google said patches for security issues and breaking browser bugs would stop on that December date. The last v1.8.3 tarball on the 1.x repo is tagged 7 April 2022.Leave 1.x, and lock the hatches on the app you cannot leave this quarter.

As of 22 August 2026, docs.angularjs.org. The $sce and ngBindHtml pages failed to load. I am not pretending that site is a live first-party manual. The names and comments below come from the 1.8.3 ngBind.js and sce.js files from GitHub. Keep the XSS guide next to this page for encoding and CSP. Read React hatches if the rewrite target is React, input validation for the server that still accepts the HTML field, and the secure coding checklist for the rest.

31 December 2021 was the last official patch day

AngularJS is the 1.x framework. Angular is the 2+ rewrite. They share a marketing word. They do not share a compiler, a template language, or a patch train. A package.json that says "angular": "^1.8.3" is the dead line. A @angular/core dependency is not.

WhatDateWho patches
LTS extendedJuly 2020Google, to 31 Dec 2021
Official LTS ends31 Dec 2021nobody upstream
1.8.3 tag7 Apr 2022last 1.x tarball
HeroDevs NES1 Jan 2022 onpaid fork

The February 2021 blog is honest about the leftover case: apps that already work will keep running, npm and bower tarballs stay up, and a compliance team that requires a supported vendor can buy HeroDevs Never-Ending Support from 1 January 2022. That is a commercial backport. It is not an Angular team release. If you pay for it, pin the exact NES version in the lockfile and still grep the hatches below. If you do not pay, treat every new 1.x CVE as unpatched.

A CDN copy of angular.min.js 1.8.3 from 2022 is the same story. Browsers moved. The framework did not. CSP, Trusted Types, and cookie defaults from 2024 to 2026 landed around it, not in it.

Migrate to Angular. Do not rename the folder

The same blog names the intended path: upgrade to Angular, using ngUpgrade so 1.x and Angular run in one app while you move modules. That is an incremental rewrite, not a flag. Templates, DI, and change detection are different products. A “just bump the major” ticket will fail review.

Practical order I want on the ticket, not a vendor pitch:

  1. Inventory every angular.module, every ng-bind-html, and every $sce.trustAs call. That list is the XSS surface you will re-implement.
  2. Stop adding 1.x routes. New screens go to Angular, or to whatever the org actually chose. React is a valid target. See the React hatch page if that is the choice.
  3. Move one routed island with ngUpgrade if you are staying in the Angular family. Keep the 1.x shell only as long as an island remains.
  4. Delete angular.js from the bundle when the last 1.x module is gone. A leftover script tag is the next audit finding.

If the org cannot fund a rewrite this year, the leftover 1.x app still has to fail closed on HTML. The next three sections are that holding pattern. They are not a reason to start a new 1.x service. A hybrid ngUpgrade app has two template languages. Review both. An Angular island that uses bypassSecurityTrustHtml is the same hatch with a later name. Do not carry the 1.x unsafe filter across the boundary.

SCE already escapes. ng-bind is the text path

Strict Contextual Escaping is on by default in 1.x. Interpolation {{ bio }} and ng-bind are the text path. I read that tag’s ngBind directive: the watcher writes element.textContent. Markup in bio stays characters. That is CWE-79 closed at the sink, for that sink.

<!-- text path: AngularJS writes textContent -->
<p ng-bind="bio"></p>
<p>{{ bio }}</p>

Prefer ng-bind over raw {{ }} if the template can flash before compile. ngCloak is the other cover. Neither one is an HTML sink.

SCE also has URL and resource-URL contexts. ng-include and a dynamic templateUrl are resource URLs. A user-controlled template URL is a loader, not a string. Allowlist the template name on the server and in $sceDelegateProvider.resourceUrlWhitelist. I will not paste a wildcard ** matcher. self is the default that stays on your origin.

Text stays text. The HTML sink sanitizes. trustAsHtml turns the sanitizer off.
TEXT ng-bind="bio" or {{ bio }}
 element.textContent
 the browser shows characters

HTML ng-bind-html="bio"
 needs ngSanitize
 $sanitize, then element.html

HATCH $sce.trustAsHtml(bio)
 getTrustedHtml returns bio as-is
 a filter named unsafe is this call

ng-bind-html plus trustAsHtml is the hatch

The ngBindHtml comment in that same tag is the contract. The directive inserts HTML. By default it wants $sanitize from the ngSanitize module, which is not in core. You add angular-sanitize.js and list ngSanitize on the module. If $sanitize is missing and the value is not already trusted, 1.x throws. The comment says that throw is instead of an exploit.

The same comment names the bypass: bind to a value you marked with $sce.trustAsHtml. The watcher then calls $sce.getTrustedHtml and element.html(...). A trusted wrapper skips sanitization. That is the hatch.

// text path in a controller. No $sce.
function BioController($scope) {
 $scope.bio = "";
}

angular.module("app", ["ngSanitize"]).controller("BioController", [
 "$scope",
 BioController,
]);
<!-- HTML sink: sanitizer on, no trust wrapper -->
<div ng-bind-html="bio"></div>

BioController is the named controller. It never injects $sce. The template may use ng-bind-html only for HTML the server already sanitized, or for a constant the module owns. A markdown field the user typed is not that, unless you ran a sanitizer you chose on the server and you still let ngSanitize run again.

// BAD: do not ship
// $scope.bio = $sce.trustAsHtml($scope.bio);
// app.filter("unsafe", function ($sce) { return $sce.trustAsHtml; });

The filter named unsafe, trust, or raw is the hatch with a worse audit trail. Stack Overflow still teaches it as the 1.2 replacement for ng-bind-html-unsafe. That old directive is gone. Do not bring it back. Grep the filter names.

$compile and element.html skip the template

SCE only sees values that go through its contexts. A controller that does element.html(scope.bio), element.append(scope.bio), or element.after(scope.content) is writing the DOM itself. AngularJS never runs getTrustedHtml.

// BAD: do not ship
// element.html(scope.bio);
// $compile(scope.bio)(scope);

$compile on a user string is worse than a bind. It turns markup into directives. A stranger who can store HTML can store an ng-include or a custom directive that reads a scope you did not mean to show. Keep templates as files. Compile those files. Do not compile the database.

ng-attr-href still needs an allowlist for schemes. Escaping will not turn a dangerous scheme into a safe link. Reject anything that is not https: or a same-origin path you wrote. A tight CSP is the leftover belt. 1.x will not set it for you. script-src with a nonce, object-src 'none', and base-uri 'none' are the headers I want on the leftover app while the rewrite runs. CSP does not replace ng-bind. It limits what a missed hatch can load.

Prove the version, then grep the hatch

You are not walking an exploit. You are proving the bundle is still 1.x, then proving the tree has no trust wrapper and no raw HTML write.

rg -n "\"angular\"\\s*:\\s*\"\\^?1\\.|angular\\.min\\.js|angular.js@" package.json bower.json
# A hit means the 1.x line is still in the app.

rg -n "trustAsHtml|trustAs\\(|ng-bind-html-unsafe|\\|\\s*unsafe|\\|\\s*trust\\b|element\\.html\\(|element\\.append\\(|\\$compile\\(" \
 --glob '!node_modules' --glob '!bower_components'

A hit on trustAsHtml or a filter that returns it is a review. A hit on element.html or $compile( with a variable needs a human to say the string is a file you own. A hit on ng-bind-html without ngSanitize in the module list will throw at runtime, which is the fail-closed path. Add ngSanitize or, better, switch that node to ng-bind.

rg -n "ngSanitize|angular-sanitize" --glob '!node_modules'
# If ng-bind-html exists and this is empty, the app throws or someone already trusted the string.

Then schedule the migration ticket. Grep is a holding pattern. It is not support. A quarterly audit that only lists the AngularJS version without opening trustAsHtml will green-light a 1.8.3 app that already opted out of SCE. Attach the rg output to the ticket. A zero-hit report is the evidence. A hit you accepted needs an owner and a date.

If the app loads 1.x from a public CDN, pin the hash and then stop. Subresource Integrity stops a tampered angular.min.js. It does not patch 1.x. Move the file onto a host you control as part of the same change that adds CSP.

Questions we keep getting

Is AngularJS still supported in 2026?

Not by Google. Official LTS ended 31 December 2021. HeroDevs sells a fork. OpenLogic also sells support. Neither is an Angular team release. Treat unpatched 1.8.3 as a compliance finding and a rewrite trigger.

Can I stay on 1.x if I only use ng-bind?

You can reduce the HTML sink. You cannot get upstream patches, browser-break fixes, or a living dependency graph. The holding pattern on this page is for the quarter you are migrating, not for a new product.

When is trustAsHtml acceptable?

When the string is HTML your build produced and you can name the file. A translation catalog you compiled, a help snippet in the repo. Never on $http data, $routeParams, or a field the user edited.

Guy Bar-Gil

Guy Bar-Gil / About Author

Guy is a product manager at WhiteSource, where we enable software development teams to integrate open source fearlessly and without compromising agility. Before WhiteSource, Guy worked for the IDF's intelligence division, where he spent time as a combat operator and project manager. Outside of work, you can find Guy reading (everything from fiction to physics), playing and watching sports, traveling the world, and spending time with friends and family. LinkedIn