Patch reachable security advisories; add bin/security-scan #12

Merged
matchu merged 2 commits from armin-bot/impress:claude/security-backlog into main 2026-11-07 20:51:37 -08:00
Contributor

Clears the current security-advisory backlog and adds bin/security-scan, the script a daily automated watch will run.

The rule: patch what's likely to affect us, and hold what clearly can't, with the reason written down next to the ignore entry (.bundler-audit.yml for gems, .security-scan-ignore.yml for JS and news posts) so it can be reviewed and revisited.

On reachability: a closer read of the trigger conditions showed that none of the patched advisories was actually exploitable against DTI as deployed (details per row). They were patched because each fix was a cheap patch or minor bump on code we do run, not because we were exposed. Nothing here calls for a follow-up investigation.

Ruby gems (bundler-audit)

Gem Advisory Decision
addressable 2.8.8 → 2.9.2 GHSA-h27x-rffw-24p4 (High, template ReDoS), GHSA-4w5q-wg7v-w6gp, GHSA-9f42-57jx-vpp3, GHSA-j82w-28jv-3pmj (template ReDoS), GHSA-qq38-m9mg-xjj5 (normalization DoS), GHSA-8gmh-2hhj-2hv9 (dot-segment normalization), GHSA-539f-xcgw-2gjg, GHSA-9crm-g4mf-wc5j (display_uri spoofing) Patched (cheap bump), though not exploitable here: the template ReDoS needs template matching (match/extract) against an attacker's URI, and we only expand our own templates. The URLs we normalize! come from Neopets' asset data.
faraday 2.14.0 → 2.14.4 GHSA-98m9-hrrm-r99r (High, NestedParamsEncoder recursion DoS), GHSA-33mh-2634-fwr2, GHSA-5rv5-xj5j-3484 (SSRF via protocol-relative URLs) Patched (patch bump), though practically unreachable: Faraday only runs in the NeoPass login flow, with URLs from NeoPass's discovery document, so exploiting it would take a malicious or compromised NeoPass.
rdiscount 2.2.7.3 → 2.2.7.5 GHSA-6r34-94wq-jhrc (out-of-bounds read) Patched (patch bump), though not exploitable: it does parse user-written list descriptions, but the bug needs input over 2 GB (an int length overflow).
sanitize 7.0.0 GHSA-wgw9-qhqg-8hv5 (CSS allowlist bypass via nesting) Held, because we only use Sanitize::Config::BASIC, which allows no <style> elements or style attributes, so CSS is stripped and never parsed. (Trivial to patch if you'd rather not lean on that.)
devise 4.9.4 GHSA-57hq-95w6-v4fc (confirmable email-change race) Held, because :confirmable isn't enabled on AuthUser. Fix is in Devise 5 (major).
devise 4.9.4 GHSA-jp94-3292-c3xv (open redirect in timeout handler) Held, because that path only runs for the :timeout warden message, which only :timeoutable sets, and it isn't enabled.
msgpack 1.8.0 GHSA-4mrv-5p47-p938 (use-after-free) Held, because our only user is bootsnap, caching our own compiled code on local disk. No untrusted input.
bcrypt 3.1.21 GHSA-f27w-vcwj-c954 (cost=31 overflow) Held: JRuby only, and our cost is 12.

Rails itself is clean at 8.1.4.1.

JS packages (yarn npm audit)

Package Advisory Decision
@remix-run/router 1.16.0 → 1.23.4 (react-router-dom → 6.30.6) GHSA-2w69-qvjg-hvjx (High, open redirect → XSS), GHSA-2j2x-hqr9-3h42, GHSA-9jcx-v3wj-wh4m Patched (minor bump within 6.x), though not exploitable here: these cover redirects returned from loaders/actions in Data/Framework mode, and the advisory explicitly excludes declarative <BrowserRouter>, which is what we use.
react-router 6.30.6 GHSA-wrjc-x8rr-h8h6 (backslash open redirect in <Link>/useNavigate) Held, because we only navigate to paths we build, which always start with a literal /outfits/. Only fixed in v7.
react-router 6.30.6 GHSA-337j-9hxr-rhxg (SSR hydration constructor injection) Held, because it only affects Framework/Data mode with SSR. We use declarative <BrowserRouter>. Only fixed in v7.
@hotwired/turbo 8.0.4 → 8.0.23 GHSA-qppm-g56g-fpvp (Turbo Frames restore stale session cookies) Patched. The only one whose preconditions we meet (cookie session store, a Turbo Frame on items/show), but the practical impact is tiny: a logout racing an in-flight frame request could leave you logged in.
@babel/runtime 7.24.5 GHSA-968p-4wvh-cqc8 (wrapRegExp .replace ReDoS) Held, because our bundle's only wrapRegExp use (Chakra's parseGradient) calls .exec() on a constant regex, never .replace().
yaml 1.10.2 GHSA-48c2-rrv3-qjmp Held: reached via @emotion/babel-plugin → babel-plugin-macros, which only runs inside Babel. We bundle with esbuild.
express, body-parser, path-to-regexp, qs, cookie, send, serve-static, proxy-addr 15 advisories Held: devDependency, used only by bin/neopass-server (local NeoPass mock).
ESLint, @typescript-eslint and their minimatch / brace-expansion / js-yaml / flatted / ajv / cross-spawn / micromatch / picomatch / braces 25 advisories Held: lint tooling only.

Dev-only holds live under yarn_development:, which only applies while the advisory is reachable solely via devDependencies. If one of those packages ever lands in production deps, the scan reports it again.

Ruby itself

We run Ruby 3.4.5. 3.4.12 is out, and two security posts landed since:

  • Regexp compilation memory corruption (GitLab's CVE-2026-89078 et al.): held, because it needs Regexp.new on attacker-supplied patterns, and DTI never compiles regexes from user input.
  • resolv CVE-2026-80212 / CVE-2026-80213: held. Async's scheduler (so Falcon) does resolve DNS via Resolv, but the attacks need us to look up an attacker-chosen hostname, and we only fetch fixed hosts (Neopets, NeoPass, Lebron, Impress 2020).

Neither is urgent, but I'd recommend moving to 3.4.12 on the next convenient deploy, via the ruby_version_latest/ruby_version_live dance in deploy/deploy.yml. bundler-audit can't see Ruby or its default gems, which is why the scan reads the Ruby news feed. It also prints a non-failing note while we're behind the latest patch release.

bin/security-scan

bin/security-scan [--since HOURS] [--osv] [--no-update]
  • bundler-audit (now in the Gemfile, dev/test group, as Rails 8.1 apps do). Runs unfiltered, then applies .bundler-audit.yml itself, so it can also report stale ignore entries.
  • yarn npm audit, run twice (--all and --environment production) so each JS finding is tagged scope: production|development. Gems get the same tag from Gemfile groups.
  • Rails security announcements and Ruby news RSS: posts in the last 7 days (configurable; reviewed posts go in the feeds: ignore list). Ruby posts are filtered to security posts and new releases in our 3.4 series.
  • --osv adds CVE aliases and fixed versions from api.osv.dev.
  • Prints compact JSON. Exits 0 when clear, 1 on new findings, and 2 if a check failed.
  • Built so failures can't pass as all clear. Each of these becomes incomplete / exit 2:
    • a feed URL that returns a non-RSS page or an empty feed
    • an audit that "succeeds" but makes every ignore entry stale (i.e. returned nothing)
    • a Ruby advisory DB more than 7 days old
    • an unknown bundle-audit result type
    • a crash, including a broken bundle or a bad flag, which still prints JSON

Right now it reports clear with 49 advisories ignored.

Testing

  • bundle exec rspec: 383 examples, 0 failures.
  • yarn build:dev rebuilt fine.
  • Browser smoke test on the dev server (Playwright): logged in and out via Devise; rendered a closet list whose description had Markdown, <script>, <style> and a javascript: link (Markdown rendered, the rest stripped); Turbo Drive navigation (page state survived, so it was a Drive visit); and the wardrobe at /outfits/new, where React Router mounted and rewrote the URL via navigate(…, { replace: true }). The wardrobe's "no data for this pet color" toast is expected: the dev DB has no pet data and Impress 2020 isn't running.
  • Exercised the scan's failure paths by temporarily removing ignore entries and adding a bogus one: it reported the un-ignored findings with scope and OSV fix versions, flagged the stale entry, and exited 1.

Closet list, logged in (Markdown rendered; the <script>, <style> and javascript: link from the description are stripped):

closet list

Wardrobe (React Router mounted and rewrote the URL; the error toast is expected with an empty dev DB):

wardrobe

🤖 Generated with Claude Code

https://claude.ai/code/session_014PWWczv69upP3Ha8B5J3zF

Clears the current security-advisory backlog and adds `bin/security-scan`, the script a daily automated watch will run. The rule: **patch** what's likely to affect us, and **hold** what clearly can't, with the reason written down next to the ignore entry (`.bundler-audit.yml` for gems, `.security-scan-ignore.yml` for JS and news posts) so it can be reviewed and revisited. **On reachability:** a closer read of the trigger conditions showed that none of the patched advisories was actually exploitable against DTI as deployed (details per row). They were patched because each fix was a cheap patch or minor bump on code we do run, not because we were exposed. Nothing here calls for a follow-up investigation. ## Ruby gems (bundler-audit) | Gem | Advisory | Decision | |---|---|---| | addressable 2.8.8 → **2.9.2** | GHSA-h27x-rffw-24p4 (High, template ReDoS), GHSA-4w5q-wg7v-w6gp, GHSA-9f42-57jx-vpp3, GHSA-j82w-28jv-3pmj (template ReDoS), GHSA-qq38-m9mg-xjj5 (normalization DoS), GHSA-8gmh-2hhj-2hv9 (dot-segment normalization), GHSA-539f-xcgw-2gjg, GHSA-9crm-g4mf-wc5j (display_uri spoofing) | **Patched** (cheap bump), though not exploitable here: the template ReDoS needs template *matching* (`match`/`extract`) against an attacker's URI, and we only `expand` our own templates. The URLs we `normalize!` come from Neopets' asset data. | | faraday 2.14.0 → **2.14.4** | GHSA-98m9-hrrm-r99r (High, NestedParamsEncoder recursion DoS), GHSA-33mh-2634-fwr2, GHSA-5rv5-xj5j-3484 (SSRF via protocol-relative URLs) | **Patched** (patch bump), though practically unreachable: Faraday only runs in the NeoPass login flow, with URLs from NeoPass's discovery document, so exploiting it would take a malicious or compromised NeoPass. | | rdiscount 2.2.7.3 → **2.2.7.5** | GHSA-6r34-94wq-jhrc (out-of-bounds read) | **Patched** (patch bump), though not exploitable: it does parse user-written list descriptions, but the bug needs input over 2 GB (an `int` length overflow). | | sanitize 7.0.0 | GHSA-wgw9-qhqg-8hv5 (CSS allowlist bypass via nesting) | **Held**, because we only use `Sanitize::Config::BASIC`, which allows no `<style>` elements or `style` attributes, so CSS is stripped and never parsed. (Trivial to patch if you'd rather not lean on that.) | | devise 4.9.4 | GHSA-57hq-95w6-v4fc (confirmable email-change race) | **Held**, because `:confirmable` isn't enabled on `AuthUser`. Fix is in Devise 5 (major). | | devise 4.9.4 | GHSA-jp94-3292-c3xv (open redirect in timeout handler) | **Held**, because that path only runs for the `:timeout` warden message, which only `:timeoutable` sets, and it isn't enabled. | | msgpack 1.8.0 | GHSA-4mrv-5p47-p938 (use-after-free) | **Held**, because our only user is bootsnap, caching our own compiled code on local disk. No untrusted input. | | bcrypt 3.1.21 | GHSA-f27w-vcwj-c954 (cost=31 overflow) | **Held**: JRuby only, and our cost is 12. | Rails itself is clean at 8.1.4.1. ## JS packages (yarn npm audit) | Package | Advisory | Decision | |---|---|---| | @remix-run/router 1.16.0 → **1.23.4** (react-router-dom → **6.30.6**) | GHSA-2w69-qvjg-hvjx (High, open redirect → XSS), GHSA-2j2x-hqr9-3h42, GHSA-9jcx-v3wj-wh4m | **Patched** (minor bump within 6.x), though not exploitable here: these cover redirects returned from loaders/actions in Data/Framework mode, and the advisory explicitly excludes declarative `<BrowserRouter>`, which is what we use. | | react-router 6.30.6 | GHSA-wrjc-x8rr-h8h6 (backslash open redirect in `<Link>`/`useNavigate`) | **Held**, because we only navigate to paths we build, which always start with a literal `/outfits/`. Only fixed in v7. | | react-router 6.30.6 | GHSA-337j-9hxr-rhxg (SSR hydration constructor injection) | **Held**, because it only affects Framework/Data mode with SSR. We use declarative `<BrowserRouter>`. Only fixed in v7. | | @hotwired/turbo 8.0.4 → **8.0.23** | GHSA-qppm-g56g-fpvp (Turbo Frames restore stale session cookies) | **Patched.** The only one whose preconditions we meet (cookie session store, a Turbo Frame on `items/show`), but the practical impact is tiny: a logout racing an in-flight frame request could leave you logged in. | | @babel/runtime 7.24.5 | GHSA-968p-4wvh-cqc8 (wrapRegExp `.replace` ReDoS) | **Held**, because our bundle's only `wrapRegExp` use (Chakra's `parseGradient`) calls `.exec()` on a constant regex, never `.replace()`. | | yaml 1.10.2 | GHSA-48c2-rrv3-qjmp | **Held**: reached via `@emotion/babel-plugin` → babel-plugin-macros, which only runs inside Babel. We bundle with esbuild. | | express, body-parser, path-to-regexp, qs, cookie, send, serve-static, proxy-addr | 15 advisories | **Held**: devDependency, used only by `bin/neopass-server` (local NeoPass mock). | | ESLint, @typescript-eslint and their minimatch / brace-expansion / js-yaml / flatted / ajv / cross-spawn / micromatch / picomatch / braces | 25 advisories | **Held**: lint tooling only. | Dev-only holds live under `yarn_development:`, which only applies while the advisory is reachable solely via devDependencies. If one of those packages ever lands in production deps, the scan reports it again. ## Ruby itself We run Ruby 3.4.5. 3.4.12 is out, and two security posts landed since: - **Regexp compilation memory corruption** (GitLab's CVE-2026-89078 et al.): **held**, because it needs `Regexp.new` on attacker-supplied patterns, and DTI never compiles regexes from user input. - **resolv** CVE-2026-80212 / CVE-2026-80213: **held**. Async's scheduler (so Falcon) does resolve DNS via `Resolv`, but the attacks need us to look up an attacker-chosen hostname, and we only fetch fixed hosts (Neopets, NeoPass, Lebron, Impress 2020). Neither is urgent, but I'd recommend moving to 3.4.12 on the next convenient deploy, via the `ruby_version_latest`/`ruby_version_live` dance in `deploy/deploy.yml`. bundler-audit can't see Ruby or its default gems, which is why the scan reads the Ruby news feed. It also prints a non-failing note while we're behind the latest patch release. ## `bin/security-scan` ``` bin/security-scan [--since HOURS] [--osv] [--no-update] ``` - **bundler-audit** (now in the Gemfile, dev/test group, as Rails 8.1 apps do). Runs unfiltered, then applies `.bundler-audit.yml` itself, so it can also report **stale ignore entries**. - **`yarn npm audit`**, run twice (`--all` and `--environment production`) so each JS finding is tagged `scope: production|development`. Gems get the same tag from Gemfile groups. - **Rails security announcements** and **Ruby news** RSS: posts in the last 7 days (configurable; reviewed posts go in the `feeds:` ignore list). Ruby posts are filtered to security posts and new releases in our 3.4 series. - **`--osv`** adds CVE aliases and fixed versions from api.osv.dev. - Prints compact JSON. Exits **0** when clear, **1** on new findings, and **2** if a check failed. - Built so failures can't pass as **all clear**. Each of these becomes `incomplete` / exit 2: - a feed URL that returns a non-RSS page or an empty feed - an audit that "succeeds" but makes *every* ignore entry stale (i.e. returned nothing) - a Ruby advisory DB more than 7 days old - an unknown bundle-audit result type - a crash, including a broken bundle or a bad flag, which still prints JSON Right now it reports `clear` with 49 advisories ignored. ## Testing - `bundle exec rspec`: 383 examples, 0 failures. - `yarn build:dev` rebuilt fine. - Browser smoke test on the dev server (Playwright): logged in and out via Devise; rendered a closet list whose description had Markdown, `<script>`, `<style>` and a `javascript:` link (Markdown rendered, the rest stripped); Turbo Drive navigation (page state survived, so it was a Drive visit); and the wardrobe at `/outfits/new`, where React Router mounted and rewrote the URL via `navigate(…, { replace: true })`. The wardrobe's "no data for this pet color" toast is expected: the dev DB has no pet data and Impress 2020 isn't running. - Exercised the scan's failure paths by temporarily removing ignore entries and adding a bogus one: it reported the un-ignored findings with scope and OSV fix versions, flagged the stale entry, and exited 1. **Closet list, logged in** (Markdown rendered; the `<script>`, `<style>` and `javascript:` link from the description are stripped): ![closet list](https://code.openneo.net/attachments/a60e4c48-ef49-4795-9fa8-7ea07c08079d) **Wardrobe** (React Router mounted and rewrote the URL; the error toast is expected with an empty dev DB): ![wardrobe](https://code.openneo.net/attachments/1bdc9bca-323f-42cd-8d64-61ecca7cb697) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014PWWczv69upP3Ha8B5J3zF
Triage of the current advisory backlog:

- Patch addressable (2.9.2), faraday (2.14.4), rdiscount (2.2.7.5),
  react-router-dom (6.30.6, @remix-run/router 1.23.4) and
  @hotwired/turbo-rails (8.0.23), since we use the affected code paths.
- Hold the rest, each with a written reason: Ruby gems in
  .bundler-audit.yml, JS packages and reviewed news posts in
  .security-scan-ignore.yml.

Add bin/security-scan, which runs bundler-audit and `yarn npm audit`,
checks the Rails security and Ruby news feeds, and prints a JSON summary
of only new, un-ignored findings (exit 1 when there are any, 2 when a
check failed). It's meant to be run daily by an automated watch.

Also add bundler-audit to the Gemfile (dev/test), as Rails 8.1 apps do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014PWWczv69upP3Ha8B5J3zF
From review: treat non-RSS/empty feeds, wholesale-stale ignore lists, a
stale advisory DB, unknown bundle-audit result types and startup crashes
as incomplete (exit 2, still JSON). Widen the feed window to 7 days and
pass --all to both yarn audits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014PWWczv69upP3Ha8B5J3zF
matchu merged commit b6463537ae into main 2026-11-07 20:51:37 -08:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
OpenNeo/impress!12
No description provided.