← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-27970 · CWE-79 · Disclosed 2026-02-26

Angular is a development platform for building mobile and desktop web applications using…

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is less an unlocked front door than a poisoned shipment that only hurts you after it gets unpacked

CVE-2026-27970 is an Angular i18n XSS in ICU message handling: malicious HTML embedded in translated content can survive into the rendered app and execute JavaScript in the app's origin. Authoritative ranges are broad on paper but narrow in practice: @angular/core is affected at <=18.2.14, >=19.0.0-next.0 <19.2.19, >=20.0.0-next.0 <20.3.17, >=21.0.0-next.0 <21.1.6, and >=21.2.0-next.0 <21.2.0, with fixes in 19.2.19, 20.3.17, 21.1.6, and 21.2.0.

The vendor baseline of MEDIUM 6.1 is still too generous for most enterprise patch queues because this is not an arbitrary-user, internet-triggerable XSS. The attacker has to tamper with a translation artifact or the localization supply chain first, the app must actually use Angular i18n plus ICU messages, and a sane CSP or Trusted Types policy can blunt or stop the payoff; those are heavy real-world friction points.

"This is a real XSS, but it needs a compromised translation pipeline first, so for most teams it is backlog work, not a fire drill."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Compromise the translation pipeline

The attacker needs write access to the translation source or delivery path: a localization vendor portal, translation repository, CI artifact, or the XLIFF/XTB file before it is merged. There is no known weaponized one-click exploit for this CVE; the practical 'tool' is whatever already gave the attacker supply-chain or insider access.
Conditions required:
  • Attacker can alter translation files or the process that delivers them
  • Victim organization consumes third-party or otherwise untrusted translation content
Where this breaks in practice:
  • This is already a prior-compromise condition, not first-hop internet reachability
  • Many enterprises keep localization assets in version control with review trails
  • Artifact signing, branch protection, and vendor access controls can break the chain early
Detection/coverage: Traditional vuln scanners cannot see this prerequisite. Detection lives in SCM audit logs, CI/CD integrity monitoring, and vendor access telemetry.
STEP 02

Plant a malicious ICU payload

Using a crafted ICU translation string, the attacker injects HTML that Angular failed to sanitize correctly in this code path. The fix PR is explicitly fix(core): block creation of sensitive URI attributes from ICU messages in Angular PR #67183, which tells you the vulnerable path is specific, not a broad template-compilation failure.
Conditions required:
  • Application uses Angular i18n
  • At least one affected ICU message exists and is rendered at runtime
Where this breaks in practice:
  • A lot of Angular apps do not use i18n at all
  • Even among i18n users, not every app uses exploitable ICU constructs in rendered paths
  • Static SCA may flag the package version but cannot prove the feature is in use
Detection/coverage: SCA/SBOM tools catch vulnerable @angular/core versions, but not whether ICU messages are present or reachable. Template and translation-file review is required.
STEP 03

Ship the poisoned translation into production

The malicious translation has to survive code review, build, and release so it ends up inside the deployed application bundle or translation assets. This turns the exploit from 'library bug' into 'library bug plus release-process failure,' which is why the reachable population is much smaller than the package install base.
Conditions required:
  • Compromised translation content is accepted into a build or runtime translation asset store
  • Deployment promotes the affected Angular version with that content intact
Where this breaks in practice:
  • Release approvals, content QA, and localization review often inspect visible string changes
  • Build pipelines may pin hashes or compare translation artifacts between releases
Detection/coverage: CI artifact diffing and content-signature checks can detect drift. Network exposure scanners are basically useless here because Angular is a client library, not a listening service.
STEP 04

Render the ICU message in a browser session

A user must load the affected page and trigger rendering of the poisoned ICU message. If the app lacks a strict CSP and does not enforce Trusted Types, the payload can run as JavaScript in the application's origin and then steal tokens, read DOM data, or alter application behavior.
Conditions required:
  • A user visits the affected page
  • The page renders the compromised ICU message
  • CSP/Trusted Types do not block the injected execution path
Where this breaks in practice:
  • User interaction is required
  • Strict CSP can block inline or unauthorized script execution
  • Trusted Types reduces the browser-side abuse surface for DOM sinks
Detection/coverage: Browser CSP violation reports, RUM telemetry, and front-end error reporting can show blocked attempts. Most infrastructure scanners will miss the exploit entirely.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo authoritative public evidence of active exploitation found in CISA KEV or the vendor advisory as of this assessment.
Proof-of-concept availabilityI found patch material and advisory detail, but no mainstream public exploit repo or Metasploit-style weaponization. The closest technical breadcrumb is Angular PR #67183.
EPSS0.00055 (~0.055%), roughly low-teens to high-teens percentile depending on daily feed timing; that is background noise, not pressure from observed attacker interest.
KEV statusNot listed in CISA's Known Exploited Vulnerabilities Catalog. No due date, no federal exploitation signal.
CVSS vector reality checkVendor/NVD CVSS v3.1 is AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N at 6.1 MEDIUM. The overstatement is AV:N: technically true, operationally misleading, because exploitation really starts with translation-file compromise.
Affected versions<=18.2.14, 19.0.0-next.0 through 19.2.18, 20.0.0-next.0 through 20.3.16, 21.0.0-next.0 through 21.1.5, and 21.2.0-next.0 through 21.2.0-rc.0 per the GitHub advisory and OSV.
Fixed versionsUpgrade to 19.2.19, 20.3.17, 21.1.6, or 21.2.0. Ubuntu and Debian trackers list the CVE, but this is primarily an application dependency issue rather than a typical distro-network-service exposure.
Exposure/scanning realityShodan/Censys-style external exposure counts are not meaningful here because @angular/core is a browser-side library, not an internet-facing daemon. Reachability must be established through SBOM/SCA plus code/search for Angular i18n and ICU usage.
Disclosure datePublic disclosure clustered around 2026-02-25 to 2026-02-26 depending on source timezone: GitHub shows publication on 2026-02-25, while NVD/Snyk record publication/disclosure on 2026-02-26.
Reporter / originating sourcePublic CVE source is GitHub/GHSA for angular/angular. The public advisory does not clearly credit an external reporter.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to LOW (3.2/10)

The decisive factor is that exploitation requires prior compromise of the translation supply chain or translation files before the Angular bug matters. That makes this a post-compromise amplifier for a subset of multilingual Angular apps, not a broadly reachable initial-access issue.

HIGH Exploit-chain friction is materially higher than the vendor MEDIUM baseline suggests
MEDIUM Enterprise prevalence of exploitable Angular i18n + ICU deployments

Why this verdict

  • Requires a prior foothold in the translation pipeline: attacker position is effectively supply-chain, insider, or already-compromised CI/repo access, which implies the attacker is past the hardest part already. That is a major downward adjustment from the vendor's network-reachable framing.
  • Feature-gated exposure: only apps using Angular i18n, using ICU messages, and rendering those messages are in scope. That sharply cuts the real exposed population compared with raw @angular/core install counts.
  • Modern browser defenses can stop the payoff: strict CSP and Trusted Types are specifically named by Angular as mitigations. Those are not universal, but where present they add another choke point after the package-level flaw.

Why not higher?

It is not higher because there is no evidence of broad in-the-wild exploitation, no unauthenticated remote trigger from the open internet, and no server-side compromise path. The blast radius is also application-specific: a poisoned translation affects the app that shipped it, not every Angular host on the network by default.

Why not lower?

It is not IGNORE because successful exploitation still yields real same-origin JavaScript execution with credential theft and user-session abuse potential. If you run multilingual customer-facing Angular apps and outsource translations, the supply-chain prerequisite is plausible enough that the issue deserves inventorying and a normal patch cycle.

05 · Compensating Control

What to do — in priority order.

  1. Enforce strict CSP and Trusted Types — Apply this first where you control the front-end hosting stack, because Angular explicitly calls out both controls as mitigations for this issue. For a LOW verdict there is no SLA; treat it as backlog hygiene, but prioritize any internet-facing multilingual apps before low-value internal portals.
  2. Gate translation artifacts in CI — Require review, provenance checks, and ideally signatures or hash pinning for XLIFF/XTB and related localization assets so a compromised vendor or repo cannot silently inject content. For a LOW verdict there is no SLA; add it to normal SDLC hardening work rather than emergency change windows.
  3. Inventory Angular i18n and ICU usage — Do not burn cycles patching every Angular project blindly. Find the projects that actually use i18n and ICU-rendered messages, then prioritize upgrades there first; for LOW there is no SLA, so this is targeted backlog triage.
  4. Upgrade affected Angular branches — Move impacted apps to 19.2.19, 20.3.17, 21.1.6, or 21.2.0 depending on branch. For LOW there is no SLA and this should ride the next sane application maintenance cycle after the exposed multilingual apps are identified.
What doesn't work
  • A perimeter WAF does not meaningfully solve this because the dangerous payload is typically embedded in shipped translation content, not delivered as a classic inbound HTTP attack string.
  • Endpoint EDR on developer laptops will not reliably prevent browser-side same-origin JavaScript execution in end-user sessions after a bad translation has shipped.
  • Package-only SCA is insufficient by itself because it flags version exposure but cannot tell you whether the app uses Angular i18n, ICU messages, or has CSP/Trusted Types in place.
06 · Verification

Crowdsourced verification payload.

Run this on a source checkout, build workspace, or CI runner that has access to the application's package.json, lockfile, or node_modules. Invoke it as python3 check_cve_2026_27970.py /path/to/project; no admin rights are needed. It outputs VULNERABLE, PATCHED, or UNKNOWN and exits 0, 1, or 2 respectively.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# check_cve_2026_27970.py
# Detects whether a project appears vulnerable to CVE-2026-27970 based on @angular/core version.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import json
import os
import re
import sys
from typing import Optional, Tuple

PKG = '@angular/core'


def normalize(v: str) -> str:
    v = v.strip()
    v = re.sub(r'^[\^~<>= ]+', '', v)
    if v.startswith('v'):
        v = v[1:]
    return v.strip()


def parse_version(v: str) -> Optional[Tuple[int, int, int, str]]:
    v = normalize(v)
    m = re.match(r'^(\d+)\.(\d+)\.(\d+)(?:[-+]([A-Za-z0-9.-]+))?$', v)
    if not m:
        return None
    major = int(m.group(1))
    minor = int(m.group(2))
    patch = int(m.group(3))
    suffix = m.group(4) or ''
    return (major, minor, patch, suffix)


def cmp_ver(a: Tuple[int, int, int, str], b: Tuple[int, int, int, str]) -> int:
    for i in range(3):
        if a[i] < b[i]:
            return -1
        if a[i] > b[i]:
            return 1
    # Stable release is newer than prerelease with same numeric version
    if a[3] == b[3]:
        return 0
    if a[3] == '':
        return 1
    if b[3] == '':
        return -1
    return -1 if a[3] < b[3] else (1 if a[3] > b[3] else 0)


def in_range(vs: str) -> Optional[bool]:
    v = parse_version(vs)
    if not v:
        return None

    # Vulnerable ranges from GHSA/OSV:
    # <=18.2.14
    # >=19.0.0-next.0 <19.2.19
    # >=20.0.0-next.0 <20.3.17
    # >=21.0.0-next.0 <21.1.6
    # >=21.2.0-next.0 <21.2.0

    if v[0] <= 18:
        return cmp_ver(v, parse_version('18.2.14')) <= 0

    if v[0] == 19:
        return cmp_ver(v, parse_version('19.0.0-next.0')) >= 0 and cmp_ver(v, parse_version('19.2.19')) < 0

    if v[0] == 20:
        return cmp_ver(v, parse_version('20.0.0-next.0')) >= 0 and cmp_ver(v, parse_version('20.3.17')) < 0

    if v[0] == 21:
        if cmp_ver(v, parse_version('21.0.0-next.0')) >= 0 and cmp_ver(v, parse_version('21.1.6')) < 0:
            return True
        if cmp_ver(v, parse_version('21.2.0-next.0')) >= 0 and cmp_ver(v, parse_version('21.2.0')) < 0:
            return True
        return False

    return False


def read_json(path: str):
    try:
        with open(path, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return None


def find_version_in_package_json(root: str) -> Optional[str]:
    data = read_json(os.path.join(root, 'package.json'))
    if not data:
        return None
    for section in ('dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'):
        deps = data.get(section, {})
        if isinstance(deps, dict) and PKG in deps:
            return str(deps[PKG])
    return None


def find_version_in_package_lock(root: str) -> Optional[str]:
    for name in ('package-lock.json', 'npm-shrinkwrap.json'):
        data = read_json(os.path.join(root, name))
        if not data:
            continue

        packages = data.get('packages')
        if isinstance(packages, dict):
            nm_key = 'node_modules/@angular/core'
            if nm_key in packages and isinstance(packages[nm_key], dict):
                ver = packages[nm_key].get('version')
                if ver:
                    return str(ver)

        deps = data.get('dependencies')
        if isinstance(deps, dict) and PKG in deps and isinstance(deps[PKG], dict):
            ver = deps[PKG].get('version')
            if ver:
                return str(ver)
    return None


def find_version_in_node_modules(root: str) -> Optional[str]:
    pkg_json = os.path.join(root, 'node_modules', '@angular', 'core', 'package.json')
    data = read_json(pkg_json)
    if data and data.get('version'):
        return str(data['version'])
    return None


def main():
    root = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
    if not os.path.isdir(root):
        print('UNKNOWN: target path does not exist or is not a directory')
        sys.exit(2)

    version = (
        find_version_in_package_lock(root)
        or find_version_in_node_modules(root)
        or find_version_in_package_json(root)
    )

    if not version:
        print('UNKNOWN: could not determine @angular/core version from lockfile, node_modules, or package.json')
        sys.exit(2)

    verdict = in_range(version)
    if verdict is None:
        print(f'UNKNOWN: found @angular/core version {version!r} but could not parse it')
        sys.exit(2)
    elif verdict:
        print(f'VULNERABLE: detected @angular/core {version}')
        sys.exit(1)
    else:
        print(f'PATCHED: detected @angular/core {version}')
        sys.exit(0)


if __name__ == '__main__':
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not treat this like an emergency fleet-wide patch wave. Identify the Angular apps that are both multilingual and actually use i18n/ICU rendering, confirm whether they lack CSP/Trusted Types, and patch those first; for a LOW verdict there is no noisgate mitigation SLA and no noisgate remediation SLA — treat as backlog hygiene. If you outsource translations, add artifact review/provenance checks in the same maintenance cycle and roll the framework upgrade into the next normal application release rather than burning an out-of-band change window.

Sources

  1. NVD CVE record
  2. GitHub Security Advisory GHSA-prjf-86w9-mfqv
  3. OSV record for CVE-2026-27970
  4. Angular security best practices
  5. Angular fix pull request #67183
  6. CISA Known Exploited Vulnerabilities Catalog
  7. Snyk advisory with EPSS display
  8. Ubuntu CVE tracker
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.