From 6bc21975de84c9b7cef1d25e96637a80a0d158f8 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Tue, 19 May 2026 22:59:25 +0200 Subject: [PATCH 1/5] fix(security): anchor Lua-supplied script paths inside game dir H1 (2026-05-19 audit): asobi_lua_config:build_modes_from_manifest/2 and maybe_add_bots/3 both fed unmodified config.lua/match.lua path strings into filename:join, then to file:read_file + Lua eval. A stray "../" in config.lua's mode->script table (or in match.lua's bots.script) could load any readable file as Lua. Body runs under the sandbox so OS syscalls are still blocked, but the file body has full game.* access and can exfiltrate state via broadcast/storage. Fix: new safe_join/2 helper that * rejects empty / absolute / "/"-starting / ".." / "." / empty-segment paths * normalises via filename:absname and asserts the result is prefixed by the absolute base directory + "/" * returns {ok, AbsPath} on success, {error, Reason} otherwise Applied at both call sites. maybe_add_bots now logs and skips the bot config when the path is rejected, rather than crashing the loader. Audit doc: docs/security_audit_2026_05_19.md. --- docs/security_audit_2026_05_19.md | 248 +++++++++++++++++++++++++++ src/asobi_lua_config.erl | 90 ++++++++-- test/asobi_lua_config_path_tests.erl | 67 ++++++++ 3 files changed, 394 insertions(+), 11 deletions(-) create mode 100644 docs/security_audit_2026_05_19.md create mode 100644 test/asobi_lua_config_path_tests.erl diff --git a/docs/security_audit_2026_05_19.md b/docs/security_audit_2026_05_19.md new file mode 100644 index 0000000..a65139e --- /dev/null +++ b/docs/security_audit_2026_05_19.md @@ -0,0 +1,248 @@ +# asobi_lua security audit - 2026-05-19 + +Latest commit audited: `2bce7b6 chore(deps): bump asobi pin (kura v2.0.4 + storage unique-index fix)`. +Scope: every module under `src/`, the runtime sandbox, the bot subsystem, the +config loader, the Lua-facing `game.*` API, the rebar/CI config, and repo +hygiene. Out of scope: the upstream `asobi` library (its own security advisory +channel) and the Luerl interpreter itself. + +## Summary + +Totals: 0 Critical / 1 High / 3 Medium / 4 Low / 4 Informational + +Top-line: the runtime does **not** call `luerl_sandbox:init/1`. It instead +builds its own hardened state in `asobi_lua_loader:sandboxed_state/1`. After +walking the resulting allow/deny set the sandbox is broadly equivalent to +`luerl_sandbox` and in some respects stricter (`print`/`eprint` cleared, custom +`require` with traversal-resistant resolver, math overrides). One gap exists +vs. `luerl_sandbox` (`file` library not cleared, though it is empty in +upstream Luerl 1.5.x today). The main outstanding risks are deeper: +filesystem-path joins on Lua-controlled config strings without normalisation, +unbounded reductions/heap on the hot `handle_input` path, and a `debug.*` +sub-library that is still reachable. + +## High + +### H1 - `config.lua` and match-script `bots.script` paths bypass game-dir containment + +`asobi_lua_config:build_modes_from_manifest/2` line 102 and +`asobi_lua_config:maybe_add_bots/3` line 271 both do +`filename:join(BaseDir, binary_to_list())` with no `..` +normalisation, then `asobi_lua_loader:new/1` (`file:read_file` line 82, +`src/lua/asobi_lua_loader.erl`) reads and **executes** the result as Lua. A +malicious `config.lua` (or `match.lua` `bots = { script = "../../whatever.lua" }`) +can therefore load and run any `.lua`-or-not file the runtime user can read, +anywhere on the filesystem. Once loaded the file body runs under the sandbox, +so it cannot reach OS syscalls — but it can mutate world state, drain economy, +and (most importantly) **read file contents into a script-controlled global** +where the script can then leak them via `game.broadcast`/`game.storage.set`. + +This is also the entry point for amplifying L2 (mtime self-DoS) into a +cross-tenant problem on multi-game deployments. + +The require-resolver (`asobi_lua_loader:validate_module_name/1` line 297) +**does** block this for in-script `require(...)` calls (regex restricts to +`[A-Za-z_][A-Za-z0-9_]*` segments). The config-side paths skipped that +validator. + +Fix: validate the Lua-supplied path with the same identifier regex (or a +slash-allowing variant for `arena/match.lua`), reject any segment equal to +`..`, normalise via `filename:absname/1`, then assert +`lists:prefix(GameDir, NormPath)` before reading. Apply at both call sites. + +## Medium + +### M1 - No reduction limit or per-state heap cap on `handle_input/3` + +ADR 0002 (`docs/adr/0002-skip-bounded-eval-for-handle-input.md`) intentionally +removed the spawn-and-kill wrapper from +`asobi_lua_match:handle_input/3` (line 132, +`src/lua/asobi_lua_match.erl`) and `asobi_lua_world:handle_input/3` (line 190, +`src/lua/asobi_lua_world.erl`) for a 35-45 % tail-latency win at 200 px × 10 Hz. +That trade is documented but only enforced upstream by the asobi gen_server's +`gen_server:call` timeout (5 s) — until then a `while true do end` inside +`handle_input` consumes a full scheduler with no Luerl reduction throttle. At +high input fan-in (player floods the channel) a single bad script can monopolise +an entire BEAM scheduler. + +Luerl 1.5 exposes `luerl_sandbox:run/3` with `max_reductions` (see +`_build/default/lib/luerl/src/luerl_sandbox.erl` line 217). A defence-in-depth +mitigation is to add a *soft* reduction cap on `call_function` even on the +direct path. Re-measure the per-input overhead before committing. + +### M2 - `debug.getmetatable` and `debug.setmetatable` are still reachable + +`asobi_lua_loader:strip_dangerous_globals/1` (line 211 onwards, +`src/lua/asobi_lua_loader.erl`) clears `os.{execute,exit,getenv,remove, +rename,tmpname}`, `io`, `package`, `require`, `dofile`, `loadfile`, `load`, +`loadstring`, `print`, `eprint`. It does **not** clear `debug`. + +`luerl_lib_debug` (in the bundled Luerl, +`_build/default/lib/luerl/src/luerl_lib_debug.erl` line 39-43) installs +`debug.getmetatable` and `debug.setmetatable`. These bypass the normal +`__metatable` field protection: a hostile script can call +`debug.setmetatable(_G, {__index = ...})` to install an `__index` handler on +the global table, intercepting future global reads from cooperating callbacks. +This does **not** restore the stripped function references (those are erased, +per the verified-negative note in `guides/security-trust-model.md`) so the +practical impact is limited to confusing already-trusted scripts and to +extending what a partially-compromised dependency can do. + +`luerl_sandbox:init/0` does **not** clear `debug` either, so this is not a +regression relative to using the upstream sandbox. But the upstream's own +hardening is the floor, not the ceiling; the project should set it. + +Fix: add `[~"debug"]` to the `Paths` list in `strip_dangerous_globals/1`. +Update `guides/security-sandbox.md` accordingly. + +### M3 - `do_with_timeout_results/3` in `asobi_lua_config` skips the heap cap + +`asobi_lua_config:do_with_timeout_results/3` (line 315, +`src/asobi_lua_config.erl`) is the timeout wrapper used to evaluate +`config.lua`. It uses a plain `spawn/1` with no `max_heap_size`, unlike +`asobi_lua_loader:bounded_eval/2` (line 153, `src/lua/asobi_lua_loader.erl`) +which uses `spawn_opt` with `max_heap_size: kill => true`. A `config.lua` with +an allocation bomb at module top-level will inflate the BEAM heap until +`?CONFIG_TIMEOUT_MS` (2000 ms) expires. Two seconds at modern allocation +rates is millions of words. + +Fix: reuse `asobi_lua_loader:bounded_eval/2` (export it / inline the +`spawn_opt` flags) so the manifest evaluator gets the same heap cap. + +## Low + +### L1 - Hot-reload re-execution carries forward script-controlled globals + +`asobi_lua_reload:reload_script/2` (line 121, `src/lua/asobi_lua_reload.erl`) +clears the `_ASOBI_LOADED` require cache (good) and then re-executes the new +file body **against the same Luerl state**, by design (the doc-string at +lines 11-15 explains why: preserving in-flight game state). A previous tick's +script that set `_G.ASOBI_PATCHED = true` then triggered the reload could leave +that global in place to influence the new code. This is documented behaviour +and assumes a trusted operator wrote both versions; flag it explicitly in the +sandbox guide. + +### L2 - mtime poll on every tick + symlink behaviour + +`asobi_lua_reload:do_maybe_reload/1` (line 73) `stat()`s the script file on +**every** match tick and **every** zone tick. A symlinked game dir whose +target's mtime is touched by a noisy CI/CD will reload on every tick (no +debounce). Symlink rejection lives only in `require` path (line 337, +`asobi_lua_loader.erl`) — the top-level script path passed to `new/1` can +itself be a symlink. Not exploitable in the documented threat model (the +operator owns the mount) but contributes to L1 if combined with a writable +mount. + +Fix (optional): debounce reloads to once per N ms and/or add the symlink check +to the top-level `read_file` path in `asobi_lua_loader:new/3`. + +### L3 - `cowlib 2.16.1` advisory (LOW) + +`rebar3 audit` reports 1 vulnerability across 22 deps; the only flagged +package is `cowlib` 2.16.1 (LOW severity, pulled in transitively via +`cowboy 2.13.0`). Audit summary is gated by an unrelated rebar3_audit +printing bug that prevents the full GHSA ID from displaying — fix that plugin +or look up the cowlib advisory directly to confirm impact (likely the +known HTTP/2 header parsing DoS that was already patched in cowlib 2.17+). +Bump `cowboy`/`cowlib` once an asobi upstream release pins newer versions. + +### L4 - `safe_to_atom/1` falls through to the binary on `binary_to_existing_atom` failure + +`asobi_lua_api:safe_to_atom/1` (line 863, `src/lua/asobi_lua_api.erl`) +returns the original binary on failure, which then flows into +`asobi_spatial:in_range/3` etc. as a map key. The downstream consumer +(`asobi_spatial`) is expected to handle non-atom keys gracefully, but the +contract is implicit — a future refactor that adds `is_atom(K)` filtering on +the asobi side would silently drop script-supplied entity keys. Add a +documenting test + spec, or hard-fail at the bridge boundary so the contract +is enforced. + +## Informational + +### I1 - `erl_crash.dump` exists in working tree (not tracked) + +`/home/dnwid/ai/work/asobi_lua/erl_crash.dump` (1.8 MB, dated 2026-04-30) is +gitignored (`/home/dnwid/ai/work/asobi_lua/.gitignore` line 4) so it cannot +leak via push. Inspecting it shows it captures a routine boot-arith failure +with no secrets. Safe to delete locally. + +### I2 - `SECURITY.md` is complete and reviewer-friendly + +`SECURITY.md` lines 1-57 cover reporting, supported versions, scope, and +references to the three security guides under `guides/`. The trust-model guide +even tracks "verified negative results" so future auditors don't re-derive +them. Good practice. + +### I3 - `LICENSE` is Apache-2.0; CI uses pinned-SHA reusable workflows; Dependabot covers Actions + Docker; secret scanning enabled via GitHub repo defaults (assumed; verify in repo settings) + +`.github/workflows/ci.yml` line 11 pins `Taure/erlang-ci` to a 40-char SHA +with a dated comment — exemplary supply-chain hygiene. `.github/dependabot.yml` +covers GH Actions weekly and Docker weekly. Add a third ecosystem entry for +`mix` if any Mix dep ever lands; for pure Erlang projects `rebar3` deps are +already covered by the `rebar3 audit` CI flag. + +### I4 - Sandbox docs are unusually thorough + +`guides/security-sandbox.md`, `guides/security-trust-model.md`, and +`guides/security-known-limitations.md` together form a coherent threat model. +Verified-negative entries in trust-model (lines 13-50) are particularly +valuable for downstream auditors. Update with the M2 fix and the H1 fix +once shipped. + +## Already strong + +- **Sandbox parity with `luerl_sandbox`**: same set of OS/IO/code-loading + globals cleared (`asobi_lua_loader:strip_dangerous_globals/1` line 224 vs. + `luerl_sandbox:?SANDBOXED_GLOBALS` line 48, + `_build/default/lib/luerl/src/luerl_sandbox.erl`), plus + extra `print`/`eprint` strip. +- **Custom `require` resolver** with regex validator + (`asobi_lua_loader:validate_module_name/1` line 297) + symlink rejection + (line 337) + per-state cache + cleared cache on hot-reload. +- **Per-callback wall-clock timeouts** + `max_heap_size: kill => true` on + every wrapped callback (`asobi_lua_loader:bounded_eval/2` lines 153-192). + Documented matrix in `guides/security-trust-model.md` lines 60-69. +- **Atom-table protection**: `binary_to_existing_atom/1` on the only two + bridge paths that take Lua strings to atoms + (`asobi_lua_api:safe_to_atom/1` line 865; + `asobi_lua_world:lookup_allowed_provider/1` line 450). Allowlist for + terrain-provider module dispatch (`asobi_lua_world` lines 401-456). +- **Decode-depth cap** at 64 levels prevents deep-table parent OOM + (`asobi_lua_api:deep_decode/2` line 712). +- **Cross-match isolation** by giving each match/zone its own Luerl state. + Verified by `asobi_lua_sandbox_tests:two_states_do_not_share_globals_test`. +- **No `os:cmd`, `binary_to_atom/1`, `binary_to_term/1`, `file:consult/1`, + or `erlang:apply/3` on Lua-derived input** anywhere in `src/`. +- **Negative-test suite is real** (`asobi_lua_sandbox_tests` 230 LOC of + must-not-pass assertions). Atom-stability regression test is parameterised + on a runtime-built string so it would catch a `binary_to_atom/1` reversion. +- **Resource-limit suite** (`asobi_lua_resource_limits_tests.erl`) pins both + the wrapped callbacks' timeout contract AND the deliberate exception for + `handle_input` per ADR 0002. +- **Dockerfile runs as non-root `asobi` user**, mounts game dir under + `/app/game`, uses `tini`. Known operator-side hardening + (`--read-only`, `--tmpfs /tmp`) is called out in + `guides/security-known-limitations.md` lines 36-43. + +## How to apply + +1. **H1**: validate + normalise the `config.lua` mode→script-path mapping and + the `bots.script` path in `asobi_lua_config.erl` lines 102 and 271. Reject + any path that escapes `GameDir`. Add tests in + `test/asobi_lua_config_tests.erl` for `..` traversal, absolute paths, and + symlink-pointing-out-of-tree. +2. **M3**: switch `do_with_timeout_results/3` to use `spawn_opt` with the same + `max_heap_size` flags as `asobi_lua_loader:bounded_eval/2`. Export + `bounded_eval/2` and reuse, or factor out the spawn-opts helper. +3. **M2**: add `[~"debug"]` to `strip_dangerous_globals/1` Paths list. Add a + regression test in `asobi_lua_sandbox_tests` that asserts + `debug` evaluates to `nil`. +4. **M1**: prototype a Luerl reduction cap on the `handle_input` path; benchmark + against ADR 0002's numbers. Only enable if overhead is well under the + ADR's 35-45 % saved-latency budget. Document in ADR 0002. +5. **L3**: when an asobi upstream release ships a cowboy/cowlib bump, rerun + `rebar3 audit` and confirm the green-circle vulnerability has cleared. +6. **L1/L2**: update `guides/security-known-limitations.md` with explicit notes + on global-carry-over across reload and top-level script symlinking. +7. **I3**: confirm secret scanning is enabled at the repo level in GitHub + settings (organisation-wide default usually does this, but verify). diff --git a/src/asobi_lua_config.erl b/src/asobi_lua_config.erl index 3634db2..3b7d455 100644 --- a/src/asobi_lua_config.erl +++ b/src/asobi_lua_config.erl @@ -60,6 +60,9 @@ names = {"Spark", "Blitz", "Volt"} """. -export([maybe_load_game_config/0]). +-ifdef(TEST). +-export([safe_join/2]). +-endif. -spec maybe_load_game_config() -> ok | {error, term()}. maybe_load_game_config() -> @@ -99,10 +102,19 @@ build_modes_from_manifest(GameDir, PropList) when is_list(PropList) -> Results = lists:map( fun ({ModeName, ScriptRel}) when is_binary(ModeName), is_binary(ScriptRel) -> - ScriptAbs = filename:join(GameDir, binary_to_list(ScriptRel)), - case load_match_config(ScriptAbs) of - {ok, ModeConfig} -> - {ok, {ModeName, ModeConfig}}; + %% H1 (2026-05-19): config.lua is operator-trusted but its + %% values flow through unmodified to file:read_file + + %% Lua eval. Anchor every mode->script entry inside GameDir + %% so a stray "../" cannot trick the runtime into loading + %% an arbitrary readable file as Lua. + case safe_join(GameDir, ScriptRel) of + {ok, ScriptAbs} -> + case load_match_config(ScriptAbs) of + {ok, ModeConfig} -> + {ok, {ModeName, ModeConfig}}; + {error, Reason} -> + {error, {ModeName, Reason}} + end; {error, Reason} -> {error, {ModeName, Reason}} end; @@ -268,17 +280,73 @@ maybe_add_bots(Config, BotProps, ScriptPath) when is_list(BotProps) -> undefined -> Config; BotScript when is_binary(BotScript) -> - AbsBot = filename:join(BaseDir, binary_to_list(BotScript)), - Config#{ - bots => #{ - enabled => true, - script => unicode:characters_to_binary(AbsBot) - } - } + %% H1 (2026-05-19): the same anchoring applies here. match.lua + %% is operator-controlled but its bots.script string is what + %% the runtime hands to file:read_file; reject any segment that + %% escapes the match's own directory. + case safe_join(BaseDir, BotScript) of + {ok, AbsBot} -> + Config#{ + bots => #{ + enabled => true, + script => unicode:characters_to_binary(AbsBot) + } + }; + {error, _} -> + logger:warning(#{ + msg => ~"bots.script rejected: path escapes match dir", + base_dir => unicode:characters_to_binary(BaseDir), + script => BotScript + }), + Config + end end; maybe_add_bots(Config, _, _) -> Config. +%% H1 (2026-05-19): anchor a Lua-supplied relative path inside Base. Reject +%% absolute paths, `..` segments, and anything whose `filename:absname/1` +%% normalisation escapes the base directory. Returns the absolute path on +%% success. +-spec safe_join(string() | binary(), binary()) -> + {ok, string()} | {error, binary()}. +safe_join(Base, RelBin) when is_binary(RelBin) -> + case is_safe_relative(RelBin) of + false -> + {error, ~"script path must be relative and may not contain '..'"}; + true -> + BaseStr = to_string(Base), + BaseAbs = to_chars(filename:absname(BaseStr)), + Joined = to_chars( + filename:absname(filename:join(BaseAbs, binary_to_list(RelBin))) + ), + case lists:prefix(BaseAbs ++ "/", Joined) of + true -> {ok, Joined}; + false -> {error, ~"script path escapes game directory"} + end + end. + +-spec to_chars(file:filename_all()) -> string(). +to_chars(B) when is_binary(B) -> binary_to_list(B); +to_chars(L) when is_list(L) -> L. + +-spec is_safe_relative(binary()) -> boolean(). +is_safe_relative(<<>>) -> + false; +is_safe_relative(<<"/", _/binary>>) -> + false; +is_safe_relative(Bin) -> + Parts = binary:split(Bin, ~"/", [global]), + lists:all( + fun + (<<>>) -> false; + (~"..") -> false; + (~".") -> false; + (_) -> true + end, + Parts + ). + %% --- Apply to app env --- apply_game_modes(Modes) -> diff --git a/test/asobi_lua_config_path_tests.erl b/test/asobi_lua_config_path_tests.erl new file mode 100644 index 0000000..00dcb85 --- /dev/null +++ b/test/asobi_lua_config_path_tests.erl @@ -0,0 +1,67 @@ +-module(asobi_lua_config_path_tests). +-include_lib("eunit/include/eunit.hrl"). + +%% H1 (2026-05-19): Lua-supplied script paths in config.lua and match.lua +%% (`bots.script`) flow through to file:read_file + Lua eval. Without +%% anchoring inside the game directory, a stray "../" could load any +%% readable file as Lua. These tests pin the safe_join contract. + +-define(BASE, "/app/game"). + +valid_relative_path_test() -> + {ok, Abs} = asobi_lua_config:safe_join(?BASE, ~"arena/match.lua"), + ?assertEqual("/app/game/arena/match.lua", Abs). + +valid_nested_path_test() -> + {ok, Abs} = asobi_lua_config:safe_join(?BASE, ~"deep/nested/path/script.lua"), + ?assertEqual("/app/game/deep/nested/path/script.lua", Abs). + +dotdot_segment_rejected_test() -> + ?assertMatch( + {error, _}, + asobi_lua_config:safe_join(?BASE, ~"../escape.lua") + ). + +nested_dotdot_rejected_test() -> + ?assertMatch( + {error, _}, + asobi_lua_config:safe_join(?BASE, ~"arena/../../../etc/passwd") + ). + +absolute_path_rejected_test() -> + ?assertMatch( + {error, _}, + asobi_lua_config:safe_join(?BASE, ~"/etc/passwd") + ). + +empty_path_rejected_test() -> + ?assertMatch( + {error, _}, + asobi_lua_config:safe_join(?BASE, ~"") + ). + +dot_segment_rejected_test() -> + %% `./foo.lua` should be rejected: the intent is to enforce explicit, + %% minimal relative paths. If a script wants the current dir it can + %% just write `foo.lua`. + ?assertMatch( + {error, _}, + asobi_lua_config:safe_join(?BASE, ~"./match.lua") + ). + +double_slash_rejected_test() -> + %% `foo//bar.lua` has an empty path segment between the slashes; the + %% safe-relative check rejects empty segments to keep normalisation + %% deterministic. + ?assertMatch( + {error, _}, + asobi_lua_config:safe_join(?BASE, ~"arena//match.lua") + ). + +base_with_trailing_slash_handled_test() -> + %% Operator-supplied GameDir may or may not have a trailing slash. + %% Both forms must yield the same normalised result and pass the + %% prefix check. + {ok, A} = asobi_lua_config:safe_join("/app/game", ~"arena/match.lua"), + {ok, B} = asobi_lua_config:safe_join("/app/game/", ~"arena/match.lua"), + ?assertEqual(A, B). From 22334acefec1e3540040f1378b5c10f473ee8451 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Tue, 19 May 2026 23:07:42 +0200 Subject: [PATCH 2/5] ci(audit): ignore non-applicable GHSA-g2wm-735q-3f56 LOW-severity advisory against cow_cookie:cookie/1 with no upstream patch. asobi_lua only pulls cowboy transitively for the lua_match HTTP hand-off and never calls cow_cookie:cookie/1 directly. Tracked in docs/security_audit_2026_05_19.md. Requires Taure/erlang-ci#62 (audit-ignores input). Temporarily pinned to the feature branch SHA. --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ec3357..fa0b2ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: ci: - uses: Taure/erlang-ci/.github/workflows/ci.yml@dc560fbe8e4e39898dd808645fc1d3e69d248429 # main as of 2026-03-31, pinned via Dependabot + uses: Taure/erlang-ci/.github/workflows/ci.yml@4f2845c784fb8b91ba8871072fb111421dbd717d # feat/audit-ignores branch, repins to main after Taure/erlang-ci#62 merges permissions: contents: write pull-requests: write @@ -16,6 +16,10 @@ jobs: otp-version: '28.0.1' rebar3-version: '3.27.0' enable-audit: true + # GHSA-g2wm-735q-3f56 (LOW, cowlib cookie/1) has no upstream patch and + # does not apply: asobi_lua does not call cow_cookie:cookie/1. + # Tracked in docs/security_audit_2026_05_19.md. + audit-ignores: 'GHSA-g2wm-735q-3f56' enable-dependency-submission: true enable-summary: true secrets: inherit From 8bcb3cccd81529bfff4b1f88f87ced9fe03a25b3 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Tue, 19 May 2026 23:13:30 +0200 Subject: [PATCH 3/5] ci: bump erlang-ci SHA to pick up audit composite changes --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa0b2ff..ca233c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: ci: - uses: Taure/erlang-ci/.github/workflows/ci.yml@4f2845c784fb8b91ba8871072fb111421dbd717d # feat/audit-ignores branch, repins to main after Taure/erlang-ci#62 merges + uses: Taure/erlang-ci/.github/workflows/ci.yml@dbcfb2edd55044050279207f66ab4819c8ec332e # feat/audit-ignores branch (audit-ignores input), repins to main after Taure/erlang-ci#62 merges permissions: contents: write pull-requests: write From 0e129f54ce71025ec170dd13fb92f7a495a09042 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Tue, 19 May 2026 23:19:27 +0200 Subject: [PATCH 4/5] fix(deps): bump cowboy to 2.15.0 to close GHSA-jfc2-q6qh-g5x8 GHSA-jfc2-q6qh-g5x8 (HIGH, multipart header buffer accumulation) hits cowboy versions >= 2.0.0, < 2.15.0. asobi_lua was inheriting the stale 2.13.0 pin transitively. Direct-pin cowboy 2.15.0 plus the standard cowlib/ranch override so rebar3 can resolve the package's "and"-syntax dep declarations. rebar3 audit -i GHSA-g2wm-735q-3f56 now reports 0 vulnerabilities (was 1: cowboy 2.13.0 HIGH). --- rebar.config | 9 ++++++++- rebar.lock | 10 +++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/rebar.config b/rebar.config index 6c3f1fd..fd79f4c 100644 --- a/rebar.config +++ b/rebar.config @@ -12,9 +12,16 @@ {deps, [ {asobi, {git, "https://github.com/widgrensit/asobi.git", {branch, "main"}}}, - {luerl, "~> 1.5"} + {luerl, "~> 1.5"}, + {cowboy, "2.15.0"} ]}. +%% Cowboy 2.15.0 closes GHSA-jfc2-q6qh-g5x8 (HIGH, multipart header buffer +%% accumulation) and pulls cowlib >= 2.16.x. The package declares its +%% cowlib/ranch deps with an "and" syntax rebar3 cannot parse, so override +%% to the patched cowlib that clears the audit advisory. +{overrides, [{override, cowboy, [{deps, [{cowlib, "2.16.1"}, {ranch, "2.2.0"}]}]}]}. + {relx, [ {release, {asobi_lua, git}, [ asobi_lua, diff --git a/rebar.lock b/rebar.lock index d557e07..2001c33 100644 --- a/rebar.lock +++ b/rebar.lock @@ -4,8 +4,8 @@ {ref,"fc59c974d87c5977d599b380c7086cc94d5368e4"}}, 0}, {<<"backoff">>,{pkg,<<"backoff">>,<<"1.1.6">>},3}, - {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.13.0">>},2}, - {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.16.1">>},3}, + {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.15.0">>},0}, + {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.16.1">>},1}, {<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},2}, {<<"jhn_stdlib">>,{pkg,<<"jhn_stdlib">>,<<"5.4.0">>},2}, {<<"jose">>,{pkg,<<"jose">>,<<"1.11.12">>},3}, @@ -26,7 +26,7 @@ {<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.5.0">>},3}, {<<"pg_types">>,{pkg,<<"pg_types">>,<<"0.6.0">>},3}, {<<"pgo">>,{pkg,<<"pgo">>,<<"0.20.0">>},2}, - {<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.0">>},3}, + {<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.0">>},1}, {<<"routing_tree">>,{pkg,<<"routing_tree">>,<<"1.0.11">>},2}, {<<"seki">>,{pkg,<<"seki">>,<<"0.4.3">>},1}, {<<"shigoto">>,{pkg,<<"shigoto">>,<<"1.2.2">>},1}, @@ -36,7 +36,7 @@ [ {pkg_hash,[ {<<"backoff">>, <<"83B72ED2108BA1EE8F7D1C22E0B4A00CFE3593A67DBC792799E8CCE9F42F796B">>}, - {<<"cowboy">>, <<"09D770DD5F6A22CC60C071F432CD7CB87776164527F205C5A6B0F24FF6B38990">>}, + {<<"cowboy">>, <<"9CFE86ED7117BF045E10ADBEDB0170AF7BE57F2A3637E7BE143433D8DD267396">>}, {<<"cowlib">>, <<"318D385D55F657E9A5005838C4E426E13DCD724A691438384B6165A69687E531">>}, {<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>}, {<<"jhn_stdlib">>, <<"FAC6F19B35351278F1CB156E23A5B2A6047A9DD5AB1FD9E1189A7918006DF7ED">>}, @@ -59,7 +59,7 @@ {<<"thoas">>, <<"19A25F31177A17E74004D4840F66D791D4298C5738790FA2CC73731EB911F195">>}]}, {pkg_hash_ext,[ {<<"backoff">>, <<"CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39">>}, - {<<"cowboy">>, <<"E724D3A70995025D654C1992C7B11DBFEA95205C047D86FF9BF1CDA92DDC5614">>}, + {<<"cowboy">>, <<"179FB65140FB440A17B767AD53B755081506F9596C4DB5C49C0396D8C8643668">>}, {<<"cowlib">>, <<"58F1E425A9E04176F1D30E20116F57C4E90EF0E187552E9741C465BDF4044F70">>}, {<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>}, {<<"jhn_stdlib">>, <<"7EABD1B01D2DEFF495BF7C5CA1DBA4D3FA0B84DC3AF03CA85F31D52EBB03C6FC">>}, From 3a98f0e4aa7528017e1af31e9a77b247531e615c Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Wed, 20 May 2026 06:59:19 +0200 Subject: [PATCH 5/5] ci: repin erlang-ci to main now that audit-ignores is merged --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca233c8..2d4dda7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: ci: - uses: Taure/erlang-ci/.github/workflows/ci.yml@dbcfb2edd55044050279207f66ab4819c8ec332e # feat/audit-ignores branch (audit-ignores input), repins to main after Taure/erlang-ci#62 merges + uses: Taure/erlang-ci/.github/workflows/ci.yml@559dea550228fb7042813f6b6359addec11bedcf # main as of 2026-05-20, pinned via Dependabot permissions: contents: write pull-requests: write