CVE-2026-48593
Description
Uncontrolled Resource Consumption vulnerability in oban-bg oban_web ('Elixir.Oban.Web.CronExpr' modules) allows memory exhaustion via unbounded cron range expansion.
An attacker with access to schedule cron jobs can submit a malicious cron expression such as "0 0 1-100000000 * *". When a user with dashboard access views the cron job list, 'Elixir.Oban.Web.CronExpr':describe/1 is called to render the expression. parse_range/1 parses both range endpoints via Integer.parse/1 with no bounds check, and the downstream helpers expand_dom_parts/1 and expand_dow_parts/1 materialise the range eagerly via Enum.to_list/1, causing allocation of ~2.4 GB and stalling or crashing the BEAM node. A sibling helper extract_dom_values already validates range bounds, but the expansion helpers do not.
This issue affects oban_web: from 2.12.0 before 2.12.5.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Unbounded cron range expansion in oban_web 2.12.0–2.12.4 allows an attacker to exhaust BEAM memory via a malicious cron expression.
Vulnerability
Uncontrolled Resource Consumption vulnerability in oban-bg's oban_web package, specifically in the Elixir.Oban.Web.CronExpr module. When a cron expression is rendered (via describe/1) that contains a large range, the helpers expand_dom_parts/1 and expand_dow_parts/1 materialize the range eagerly using Enum.to_list/1 without bounds checking. An attacker with cron job scheduling access can submit a malicious expression such as "0 0 1-100000000 * *" to trigger the issue. Affected versions: from 2.12.0 before 2.12.5 [1][2][4].
Exploitation
An attacker must have the ability to schedule cron jobs in the Oban dashboard. After submitting a cron expression with an out-of-domain numeric range (e.g., 0 0 1-100000000 * *), any user with dashboard access who views the cron job list triggers describe/1. The parse_range/1 function does not validate endpoints, and the downstream expansion helpers convert the range to a list without size limits, allocating approximately 2.4 GB of memory and stalling or crashing the BEAM node [1][4]. No additional privileges or user interaction beyond viewing the list is required.
Impact
Successful exploitation results in memory exhaustion, leading to denial of service (availability impact). The CVSS v4.0 score is 5.9 (Medium) [2]. The attacker does not gain code execution or data access; the impact is limited to crashing or stalling the Oban web node.
Mitigation
The vulnerability is fixed in oban_web version 2.12.5, released with commit 9998b7e [3]. The fix adds bounds checking to the field parsing pipeline by passing valid ranges (0..59, 0..23, 1..31, 0..7) and rejects out-of-bounds values during parsing [3][4]. Users should upgrade to 2.12.5 or later. No workaround is documented for affected versions.
AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
19998b7e284e0Merge commit from fork
2 files changed · +48 −24
lib/oban/web/cron_expr.ex+21 −18 modified@@ -45,10 +45,10 @@ defmodule Oban.Web.CronExpr do defp describe_parsed(expression) do with [min, hrs, dom, "*", dow] <- String.split(expression, " ", parts: 5), - {:ok, parsed_min} <- parse_field(min), - {:ok, parsed_hrs} <- parse_field(hrs), - {:ok, parsed_dom} <- parse_field(dom), - {:ok, parsed_dow} <- parse_field(dow, @days_of_week_translations) do + {:ok, parsed_min} <- parse_field(min, 0..59), + {:ok, parsed_hrs} <- parse_field(hrs, 0..23), + {:ok, parsed_dom} <- parse_field(dom, 1..31), + {:ok, parsed_dow} <- parse_field(dow, 0..7, @days_of_week_translations) do combine_description(parsed_min, parsed_hrs, parsed_dom, parsed_dow) else _ -> nil @@ -57,11 +57,11 @@ defmodule Oban.Web.CronExpr do # Field Parsing - defp parse_field(field, translations \\ %{}) do + defp parse_field(field, bounds, translations \\ %{}) do parts = field |> String.split(",") - |> Enum.map(&parse_part(&1, translations)) + |> Enum.map(&parse_part(&1, bounds, translations)) if Enum.any?(parts, &(&1 == :error)) do :error @@ -70,37 +70,40 @@ defmodule Oban.Web.CronExpr do end end - defp parse_part("*", _translations), do: :wildcard + defp parse_part("*", _bounds, _translations), do: :wildcard - defp parse_part("*/" <> step, _translations) do + defp parse_part("*/" <> step, bounds, _translations) do case Integer.parse(step) do - {num, ""} when num > 0 -> {:step, num} + {num, ""} when num > bounds.first and num <= bounds.last -> {:step, num} _ -> :error end end - defp parse_part(part, translations) do + defp parse_part(part, bounds, translations) do if String.contains?(part, "-") do - parse_range(part, translations) + parse_range(part, bounds, translations) else - parse_value(part, translations) + parse_value(part, bounds, translations) end end - defp parse_range(part, translations) do + defp parse_range(part, bounds, translations) do with [start_str, end_str] <- String.split(part, "-", parts: 2), {:ok, start_val} <- translate_or_parse(start_str, translations), - {:ok, end_val} <- translate_or_parse(end_str, translations) do + {:ok, end_val} <- translate_or_parse(end_str, translations), + true <- start_val in bounds and end_val in bounds and end_val >= start_val do {:range, start_val, end_val} else _ -> :error end end - defp parse_value(part, translations) do - case translate_or_parse(part, translations) do - {:ok, val} -> {:value, val} - :error -> :error + defp parse_value(part, bounds, translations) do + with {:ok, val} <- translate_or_parse(part, translations), + true <- val in bounds do + {:value, val} + else + _ -> :error end end
test/oban/web/cron_expr_test.exs+27 −6 modified@@ -101,18 +101,39 @@ defmodule Oban.Web.CronExprTest do end test "returns nil for expressions with specific month" do - assert CronExpr.describe("0 0 1 1 *") == nil - assert CronExpr.describe("0 0 * 6 *") == nil + refute CronExpr.describe("0 0 1 1 *") + refute CronExpr.describe("0 0 * 6 *") end test "returns nil for invalid expressions" do - assert CronExpr.describe("invalid") == nil - assert CronExpr.describe("") == nil + refute CronExpr.describe("invalid") + refute CronExpr.describe("") + end + + test "returns nil for out of bound ranges" do + refute CronExpr.describe("0-60 * * * *") + refute CronExpr.describe("* 0-24 * * *") + refute CronExpr.describe("* * 1-32 * *") + refute CronExpr.describe("* * * 1-13 *") + refute CronExpr.describe("* * * * 1-8") + end + + test "returns nil for reversed or negative ranges" do + refute CronExpr.describe("5-1 * * * *") + refute CronExpr.describe("* * 1--100000000 * *") + refute CronExpr.describe("* * * * 5-1") + refute CronExpr.describe("0 0 1-9999999 * *") + end + + test "returns nil for out of bound steps" do + refute CronExpr.describe("*/100 * * * *") + refute CronExpr.describe("* */100 * * *") + refute CronExpr.describe("* * */100 * *") end test "returns nil for non-string input" do - assert CronExpr.describe(nil) == nil - assert CronExpr.describe(123) == nil + refute CronExpr.describe(nil) + refute CronExpr.describe(123) end test "weekdays and weekends" do
Vulnerability mechanics
Root cause
"Missing bounds validation on cron range endpoints in parse_range/1 allows unbounded eager expansion via Enum.to_list/1, causing memory exhaustion."
Attack vector
An attacker with the ability to schedule cron jobs submits a malicious expression such as `"0 0 1-100000000 * *"` [ref_id=2]. No special privilege beyond cron scheduling is required. When any user with dashboard access views the cron job list, `Oban.Web.CronExpr.describe/1` is called to render the expression. The `parse_range/1` function parses both endpoints via `Integer.parse/1` with no bounds check, and the downstream helpers `expand_dom_parts/1` and `expand_dow_parts/1` eagerly materialise the range via `Enum.to_list/1`, causing allocation of ~2.4 GB and stalling or crashing the BEAM node [ref_id=2].
Affected code
The vulnerability is in `lib/oban/web/cron_expr.ex` in the `parse_range/1` and `parse_value/1` functions, which lacked bounds validation on parsed integers. The downstream helpers `expand_dom_parts/1` and `expand_dow_parts/1` then eagerly materialise the range via `Enum.to_list/1` without any size check [ref_id=2].
What the fix does
The patch adds per-field bounds validation to `parse_field/2`, `parse_part/3`, `parse_range/3`, and `parse_value/3` by passing a range (e.g. `0..59` for minutes) as a second argument [patch_id=2590844]. In `parse_range/3`, a guard `true <- start_val in bounds and end_val in bounds and end_val >= start_val` rejects out-of-domain or reversed ranges before any expansion occurs. In `parse_value/3`, a guard `true <- val in bounds` rejects out-of-bounds single values. Step parsing in `parse_part/3` now also validates that the step falls within the field's bounds. These changes ensure that malicious expressions like `"0 0 1-100000000 * *"` return `:error` and `describe/1` returns `nil` instead of triggering memory exhaustion [patch_id=2590844][ref_id=1].
Preconditions
- authAttacker must have the ability to schedule cron jobs in the application
- inputA user with dashboard access must navigate to the cron job list view
Reproduction
Schedule a cron job with expression `"0 0 1-100000000 * *"` (or any expression with an out-of-domain range). Have any user with Oban.Web dashboard access navigate to the cron job list. The dashboard calls `describe/1` to render the expression, exhausting BEAM memory and crashing the node [ref_id=2].
Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.