Skip to content

[#146251] Fix Security Vulnerabilities (Bundle Audit + Brakeman)#178

Open
jhephs wants to merge 5 commits into
developfrom
chore/bundle-audit
Open

[#146251] Fix Security Vulnerabilities (Bundle Audit + Brakeman)#178
jhephs wants to merge 5 commits into
developfrom
chore/bundle-audit

Conversation

@jhephs

@jhephs jhephs commented Dec 9, 2025

Copy link
Copy Markdown
Collaborator

Ticket: https://reinteractive.zendesk.com/agent/tickets/146251

Overview

This PR addresses all security vulnerabilities identified by bundle audit, brakeman, and npm audit. It includes Ruby gem upgrades, 6 code-level security fixes found by Brakeman, JavaScript dependency patching, and developer tooling improvements (AI helper files, Node version pinning).

All three scanners are now clean or at known-acceptable state:

  • bundle audit: No vulnerabilities found
  • brakeman: 0 active warnings, 0 errors
  • npm audit --omit=dev: 3 remaining (jQuery/j-toker — require breaking changes or have no upstream fix)
  • bundle exec rspec: 993 examples, 0 failures

Security Vulnerabilities Fixed

Bundle Audit — Ruby Dependency CVEs

# Gem CVE / Advisory Severity Before After Issue
1 bootstrap CVE-2019-8331 Medium 4.1.3 4.3.1 XSS in tooltip/popover data-template
2 rails CVE-2025-55193, CVE-2025-24293 High 8.0.2 8.0.2.1 ANSI escape injection, unsafe Active Storage transforms
3 nokogiri Multiple libxml2 CVEs, GHSA-wx95-c6cv-8532 High 1.18.8 1.19.1 Vendored libxml2 vulnerabilities
4 rack 6 CVEs (CVE-2025-59830 thru -61919) High/Medium 2.2.17 2.2.22 DoS, info disclosure, memory exhaustion
5 faraday CVE-2026-25765 Medium 2.7.5 2.14.1 SSRF via protocol-relative URL host override
6 rexml 6 CVEs (CVE-2024-35176 thru -49761) Medium/High 3.2.6 3.4.4 Multiple DoS vulnerabilities
7 thor CVE-2025-54314 Low 1.3.2 1.4.0 Unsafe shell command construction
8 bcrypt CVE-2026-33306 Unknown 3.1.20 3.1.22 Integer overflow zero key-strengthening at cost=31
9 devise CVE-2026-32700 Unknown 4.9.4 5.0.3 Confirmable "change email" race condition

Supporting upgrade: autoprefixer-rails 8.6.5 → 10.4.21.0 (required by Bootstrap 4.3+)

Brakeman — Code Security Issues (6 Fixed)

# Type File Issue Fix
1 Mass Assignment app/controllers/api/csp_reports_controller.rb Unused params.permit! Removed unused method
2 XSS app/views/home/_hero_content.html.erb raw on user-controlled title Replaced with sanitize
3 XSS app/views/admin/cms/collections/show.html.erb raw on DB content Replaced with sanitize; removed target attr to prevent reverse-tabnabbing
4 XSS app/views/home/_hero_content.html.erb Unvalidated URL in link_to URL scheme validation; reject protocol-relative //; add rel="noopener noreferrer"
5 SQL Injection app/services/reports/user_contributions.rb User params in raw SQL Positional $N placeholders with typed QueryAttribute binds
6 File Access app/jobs/azure/speech_to_text_job.rb File.open without validation realpath check against resolved /tmp (macOS-compatible)

Brakeman Ignore Cleanup

  • Removed 4 obsolete entries for fixed issues
  • 8 remaining ignored warnings all have documented justifications
  • 0 obsolete entries remain

npm audit — JavaScript Dependencies

Auto-fixed (via npm audit fix):

  • debug (ReDoS) — upgraded
  • ws (DoS) — upgraded

Manually patched:

  • jquery-deparam (Prototype Pollution, GHSA-xg68-chx2-253g) — added __proto__/constructor/prototype key rejection in vendored source (gulp/js/vendor/jquery-deparam.js) and recompiled JS bundles. npm audit still flags this because it checks package version, not code — the vulnerability is mitigated at the source level.

Remaining (1 production vulnerability, requires breaking jQuery upgrade):

Package Severity Issue Status
jquery <= 3.4.1 Moderate Multiple XSS Requires jQuery 3.5+ (breaking — see Future Work)

Note: jQuery XSS upgrade requires replacing the dead Jcrop plugin (last updated 2013), updating j-toker, Backbone.js, and fixing 8+ deprecated API calls. This is a separate multi-day effort best handled in its own feature branch with dedicated QA.


Technical Approach

ERB Pinning (Sprockets Compatibility)

Upgrading Rails to 8.0.2.1 pulled in ERB 6.0, which is incompatible with Sprockets 3.7.x. Instead of a risky Sprockets 4.x upgrade (would require sass-rails 6.x + asset pipeline refactoring), we pinned ERB to 5.x:

gem 'erb', '~> 5.0' # Pin to 5.x to avoid Sprockets 3.x incompatibility with ERB 6.0

Devise 4.x → 5.x Upgrade

The Devise major version upgrade (CVE-2026-32700) was validated against the full test suite. This project uses :confirmable with reconfirmable: true — directly affected by the advisory. The upgrade to 5.0.3 passed all 966 specs with zero changes required.

Bundle Audit Ignore

# .bundler-audit.yml
ignore:
  - GHSA-vc8w-jr9v-vj7f  # CVE-2024-6531 — withdrawn; not a Bootstrap security issue

Code Review Hardening (2026-03-26)

After code review, the following issues were identified and fixed:

  1. Reverse-tabnabbing in collections/show.html.erb — removed target from sanitize allowed attributes
  2. Protocol-relative URL bypass in _hero_content.html.erb — regex now rejects //evil.example patterns; added rel="noopener noreferrer" to target="_blank" links
  3. exec_query bind format in user_contributions.rb — switched from named :placeholders (unsupported by exec_query) to positional $1, $2 with typed ActiveRecord::Relation::QueryAttribute binds
  4. macOS /tmp realpath in speech_to_text_job.rb — compare against Pathname.new('/tmp').realpath instead of hardcoded /tmp/ (macOS resolves to /private/tmp)
  5. copilot-instructions.md — corrected config.jsonproject.json to match actual codebase convention
  6. FD leak in speech_to_text_job.rbwav_file was opened but never closed; added wav_file.close unless wav_file.nil? || wav_file.closed? in ensure block
  7. Misleading comment in institutions_spec.rb — removed stale comment claiming the controller might re-render the edit page
  8. CSV column order in user_contributions.rbto_csv was outputting line_count before edit_count, mismatched with headers (Edits column 3, Lines column 4); swapped order
  9. rescue Exception in speech_to_text_job.rb — changed to rescue StandardError so SignalException/NoMemoryError are not swallowed; also fixed SpeechToTextService to raise RuntimeError instead of bare Exception
  10. FD leak in spec let(:wav_file)File.open in a let was never closed; added after { wav_file.close unless wav_file.closed? }
  11. README npm audit — both Security Scanning sections now consistently use npm audit --omit=dev
  12. Wrong file comment in user_contributions.rb — header said user_activity.rb; corrected to user_contributions.rb
  13. FD leak in collections_controller_specFile.open for image param replaced with fixture_file_upload
  14. RecordNotFound guard in speech_to_text_job.rb — added explicit rescue ActiveRecord::RecordNotFound before rescue StandardError so a deleted-record retry raises cleanly instead of masking with NoMethodError
  15. raise Exception in SpeechToTextService — both rescue Exception and raise Exception replaced with StandardError/RuntimeError so system-level exceptions (signals, OOM) are never swallowed by the service
  16. brakeman.ignore note for the File Access warning — updated wording to reflect the actual Pathname#realpath check (which resolves symlinks, e.g. macOS /private/tmp)
  17. allow_any_instance_of(Pathname) stubs in job spec — replaced with and_wrap_original to match only the two specific runtime paths (/tmp and *.wav), so unrelated Pathnames (like Rails.root) are not intercepted
  18. Integer param validation in user_contributions.rbcollection_id/institution_id now validated with /\A\d+\z/ before binding; non-integer values (including injection strings) are silently ignored rather than coerced via .to_i
  19. url = nil initializer in _hero_content.html.erburl was only assigned inside the institution branch; when institution is absent the later url.present? raised NameError; fixed by initializing to nil before the conditional
  20. raise e → bare raise in SpeechToTextService#recognize — using raise e resets the backtrace to the rescue line; bare raise re-raises the original exception with its original backtrace intact
  21. PostgreSQL minimum version in README.md — updated from EOL 9.5 to 14+ (9.5 reached end-of-life in 2021)
  22. brakeman.ignore SQL injection note — updated to reflect that integer IDs are validated with /\A\d+\z/ and skip the filter on invalid input (previously said .to_i coercion, which is no longer accurate)
  23. brakeman.ignore LinkToHref fingerprint — updated fingerprint/line/code for the _hero_content XSS ignore entry after the url = nil initializer shifted Brakeman’s analysis of the link_to call

All Dependencies Updated

Gem Old Version New Version
rails 8.0.2 8.0.2.1
bootstrap 4.1.3 4.3.1
autoprefixer-rails 8.6.5 10.4.21.0
nokogiri 1.18.8 1.19.1
rack 2.2.17 2.2.22
faraday 2.7.5 2.14.1
faraday-net_http 3.0.2 3.4.2
rexml 3.2.6 3.4.4
thor 1.3.2 1.4.0
bcrypt 3.1.20 3.1.22
devise 4.9.4 5.0.3
brakeman 6.1.2 8.0.2
erb (transitive) 5.1.3 (pinned)

Additional Changes

Stability Fixes (to keep specs green through upgrades)

  • TranscriptEdit.getByLine — added deterministic ordering (updated_at DESC, id DESC)
  • spec/jobs/azure/speech_to_text_job_spec.rb — updated stubs for hardened file path validation
  • spec/features/admin/institutions_spec.rb — added post-save navigation wait for flaky feature spec

Developer Tooling

  • .github/copilot-instructions.md (new) — repo-wide AI assistant context for GitHub Copilot
  • .tool-versions (updated) — pinned ruby 3.4.4 + nodejs 20.18.0 for consistent npm audit support
  • README.md — updated Security Scanning section with bundle audit update, npm audit, Node version note
  • package-lock.json — updated via npm audit fix (debug, ws patched)

Verification

$ bundle audit
No vulnerabilities found

$ bundle exec brakeman --no-pager
Security Warnings: 0
Ignored Warnings: 8 (all documented)

$ npm audit --omit=dev
3 vulnerabilities (1 moderate, 2 high)
# jquery-deparam prototype pollution mitigated in source; npm audit flags version only
# jQuery XSS requires breaking upgrade (see Future Work)

$ bundle exec rspec
993 examples, 0 failures

Testing Instructions

  1. Pull this branch
  2. bundle install && npm install
  3. Verify security scans:
    bundle audit update && bundle audit   # Should: No vulnerabilities found
    bundle exec brakeman --no-pager       # Should: Security Warnings: 0
    npm audit --omit=dev                  # Should: 3 reported (jquery-deparam patched in source; jQuery XSS needs major upgrade)
  4. Run tests:
    bundle exec rspec                     # Should: 993 examples, 0 failures
  5. Smoke test the application:
    • Homepage loads with sanitized hero content
    • Admin institution edit page works
    • Admin CMS collection descriptions render safely
    • User contributions report returns correct data
    • Sign in / sign out flows work (Devise 5.x)

Files Changed

Dependencies & Config:

  • Gemfile — gem version bumps (devise 5.x, bcrypt, brakeman 8.x, ERB pin)
  • Gemfile.lock — resolved dependencies
  • package-lock.json — npm audit fix (debug, ws)
  • .bundler-audit.yml — withdrawn CVE ignore
  • .tool-versions — pinned ruby 3.4.4 + nodejs 20.18.0

Security Fixes:

  • app/controllers/api/csp_reports_controller.rb — removed mass assignment
  • app/views/home/_hero_content.html.erb — XSS fixes (sanitize + URL validation); url=nil initializer
  • app/views/admin/cms/collections/show.html.erb — XSS fix
  • app/services/reports/user_contributions.rb — SQL injection fix; CSV column order corrected; integer param validation for collection_id/institution_id
  • app/jobs/azure/speech_to_text_job.rb — file path validation; wav_file FD closed in ensure; rescue StandardError
  • app/services/azure/speech_to_text_service.rb — raise RuntimeError instead of bare Exception; rescue StandardError instead of Exception in recognize; bare raise to preserve backtrace; removed unused => e variable; renamed unused stdout to _stdout
  • app/models/transcript_edit.rb — deterministic ordering
  • gulp/js/vendor/jquery-deparam.js — prototype pollution fix (GHSA-xg68-chx2-253g)
  • public/assets/js/* — recompiled bundles with patched jquery-deparam

Config & Documentation:

  • config/brakeman.ignore — cleaned up obsolete entries; updated File Access note to reflect realpath-based validation; updated SQL injection note to reflect integer regex validation; updated LinkToHref fingerprint after url=nil change
  • README.md — updated security scanning section; npm auditnpm audit --omit=dev throughout; PostgreSQL minimum updated from EOL 9.5 to 14+
  • .github/copilot-instructions.md — repo-wide AI assistant context (new)

Spec Fixes & Coverage:

  • spec/jobs/azure/speech_to_text_job_spec.rb — updated stubs for path validation; added invalid path (traversal) test; scoped wav_file after hook to #recognize block; added RecordNotFound test; narrowed Pathname realpath stubs with and_wrap_original
  • spec/features/admin/institutions_spec.rb — stabilized flaky test; removed stale comment
  • spec/services/reports/user_contributions_spec.rb — new: full coverage for UserContributions service; CSV column alignment tests; integer validation tests (skip filter on non-integer input)
  • spec/controllers/home_controller_spec.rb — added render_views test: home index with institution:nil renders without NameError
  • spec/models/transcript_edit_spec.rb — added getByLine ordering tests (updated_at DESC, id DESC tiebreaker)
  • spec/controllers/admin/cms/collections_controller_spec.rb — added XSS/sanitization tests; replaced File.open with fixture_file_upload

Future Work

  1. jQuery upgrade — Replace jQuery 1.x with 3.5+ or 4.x to resolve remaining XSS advisories. Requires:
    • Replacing Jcrop (dead since 2013, uses removed .bind()/.unbind() APIs)
    • Updating j-toker (.unbind(), $.parseJSON() removed in jQuery 3.x)
    • Updating Backbone.js for jQuery 3.x compatibility
    • Fixing 8 .size() calls in ERB templates
    • Full QA of Summernote, Select2, and all interactive features
  2. Sprockets → modern pipeline — Migrate to Vite/esbuild to unpin ERB and modernize assets
  3. Bootstrap 5 — Upgrade from 4.3 to 5.x for continued security support
  4. Node upgrade — Consider Node 22 LTS when Gulp compatibility allows

@jhephs jhephs changed the title Fix Security Vulnerabilities (Bundle Audit) [#146251] Fix Security Vulnerabilities (Bundle Audit) Dec 11, 2025
@jhephs jhephs changed the title [#146251] Fix Security Vulnerabilities (Bundle Audit) [#146251] Fix Security Vulnerabilities (Bundle Audit + Brakeman) Dec 11, 2025

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates dependencies and applies a set of scanner-driven security remediations (bundle-audit, Brakeman, npm audit), including targeted hardening in Rails views/services/jobs and regenerated frontend bundles.

Changes:

  • Upgraded Ruby gems (Rails, Devise, Rack, Nokogiri, etc.) and patched JS deps / vendored code to address reported CVEs.
  • Replaced/removed Brakeman-flagged patterns (e.g., raw, unsafe URL usage, raw SQL construction, unsafe file open).
  • Stabilized test suite and updated documentation/tooling to reflect the new security workflow and runtime pinning.

Reviewed changes

Copilot reviewed 18 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Gemfile Pins/bumps key gems (Rails, Devise, Brakeman, Nokogiri, ERB pin) for security compatibility.
Gemfile.lock Resolves updated gem set after security-driven upgrades.
.bundler-audit.yml Adds ignore entry for withdrawn advisory.
package-lock.json Applies npm audit fix results and refreshes lockfile metadata.
gulp/js/vendor/jquery-deparam.js Vendored mitigation for prototype pollution in jquery-deparam.
public/assets/js/default-619d4d9aa1f684a37bc97ff14eb52224.js Recompiled bundle reflecting the jquery-deparam patch.
public/assets/js/admin-8fd0995068515e7165dd569b7cade25a.js Recompiled bundle reflecting the jquery-deparam patch.
public/assets/js/templates-21b11d937a1e6949a886b8b9244bf896.js Recompiled templates bundle (ordering/contents updated by build).
config/brakeman.ignore Removes obsolete ignores and updates remaining ignore notes/fingerprints.
app/controllers/api/csp_reports_controller.rb Removes unused params.permit! mass-assignment method.
app/jobs/azure/speech_to_text_job.rb Adds realpath + /tmp constraint before opening wav file.
spec/jobs/azure/speech_to_text_job_spec.rb Updates stubs to accommodate hardened wav path validation.
spec/features/admin/institutions_spec.rb Adds navigation wait to reduce flakiness after “Save”.
app/views/home/_hero_content.html.erb Replaces raw with sanitize and adds URL scheme allowlisting.
app/views/admin/cms/collections/show.html.erb Replaces raw with sanitize for collection descriptions.
app/services/reports/user_contributions.rb Refactors raw SQL filters toward parameterization.
app/models/transcript_line.rb Tweaks best-edit selection logic (priority edits + initialization).
app/models/transcript_edit.rb Adds deterministic ordering and adjusts cache key construction.
app/views/layouts/_account.html.erb Normalizes data-* attributes into Rails data: hash form.
README.md Updates security scanning guidance and modernizes setup/workflow docs.
.github/copilot-instructions.md Adds repo-specific assistant context and conventions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/views/admin/cms/collections/show.html.erb Outdated
Comment thread .github/copilot-instructions.md Outdated
Comment thread .github/copilot-instructions.md Outdated
Comment thread app/services/reports/user_contributions.rb
Comment thread app/jobs/azure/speech_to_text_job.rb Outdated
Comment thread app/views/home/_hero_content.html.erb Outdated

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

@jhephs jhephs force-pushed the chore/bundle-audit branch from ec8b620 to a308793 Compare March 26, 2026 04:49

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 28 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants