Compare commits

..

2 Commits

Author SHA1 Message Date
Runar Ingebrigtsen
a69be52b72 fix vulnerabilities
CI / scan_ruby (push) Successful in 23s
CI / scan_js (push) Failing after 10s
CI / lint (push) Failing after 19s
CI / test (push) Failing after 16s
CI / system-test (push) Failing after 15s
2026-01-26 21:38:17 +01:00
Runar Ingebrigtsen
35f10c4bda use mise, upgrade roo 2026-01-26 21:34:44 +01:00
9 changed files with 68 additions and 24 deletions
+2
View File
@@ -0,0 +1,2 @@
[tools]
ruby = "4"
+1 -1
View File
@@ -3,7 +3,7 @@ source "https://rubygems.org"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.1.2" gem "rails", "~> 8.1.2"
# Excel import for seeds # Excel import for seeds
gem "roo", "~> 2.10" gem "roo", "~> 3.0"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft] # The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft" gem "propshaft"
# Use sqlite3 as the database for Active Record # Use sqlite3 as the database for Active Record
+11 -6
View File
@@ -111,6 +111,7 @@ GEM
concurrent-ruby (1.3.6) concurrent-ruby (1.3.6)
connection_pool (3.0.2) connection_pool (3.0.2)
crass (1.0.6) crass (1.0.6)
csv (3.3.5)
daemons (1.4.1) daemons (1.4.1)
date (3.5.1) date (3.5.1)
debug (1.11.1) debug (1.11.1)
@@ -286,9 +287,12 @@ GEM
reline (0.6.3) reline (0.6.3)
io-console (~> 0.5) io-console (~> 0.5)
rexml (3.4.4) rexml (3.4.4)
roo (2.10.1) roo (3.0.0)
base64 (~> 0.2)
csv (~> 3)
logger (~> 1)
nokogiri (~> 1) nokogiri (~> 1)
rubyzip (>= 1.3.0, < 3.0.0) rubyzip (>= 3.0.0, < 4.0.0)
rubocop (1.82.1) rubocop (1.82.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
@@ -322,7 +326,7 @@ GEM
ffi (~> 1.12) ffi (~> 1.12)
logger logger
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.4.1) rubyzip (3.2.2)
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.40.0) selenium-webdriver (4.40.0)
base64 (~> 0.2) base64 (~> 0.2)
@@ -424,7 +428,7 @@ DEPENDENCIES
propshaft propshaft
puma (>= 5.0) puma (>= 5.0)
rails (~> 8.1.2) rails (~> 8.1.2)
roo (~> 2.10) roo (~> 3.0)
rubocop-rails-omakase rubocop-rails-omakase
selenium-webdriver selenium-webdriver
solid_cable solid_cable
@@ -467,6 +471,7 @@ CHECKSUMS
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
@@ -539,7 +544,7 @@ CHECKSUMS
regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4 regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
roo (2.10.1) sha256=cbb43bc955f9c110e74b721c835fb9bd3515b63af88ec709ac87fbf30f8be70e roo (3.0.0) sha256=6fdd7a9158d657c69768b4168754ff2110cc21fdc01a1bec1010820cb05c91b1
rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273 rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
@@ -548,7 +553,7 @@ CHECKSUMS
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b
sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080 sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
@@ -44,6 +44,12 @@ class Admin::InvitationsController < Admin::BaseController
private private
def invitation_params def invitation_params
params.require(:user).permit(:email, :name, :role, :primary_language) permitted = params.require(:user).permit(:email, :name, :primary_language)
if params[:user][:role].present? && User.roles.key?(params[:user][:role])
permitted[:role] = params[:user][:role]
end
permitted
end end
end end
+16 -3
View File
@@ -3,14 +3,20 @@ class Admin::UsersController < Admin::BaseController
def index def index
@users = User.order(created_at: :desc) @users = User.order(created_at: :desc)
@users = @users.where(role: params[:role]) if params[:role].present? .by_role(params[:role])
@users = @users.where("email LIKE ?", "%#{params[:q]}%") if params[:q].present? .search_email(params[:q])
end end
def edit def edit
end end
def update def update
# Prevent users from modifying their own role
if @user == current_user && user_params[:role].present?
redirect_to admin_users_path, alert: "You cannot modify your own role."
return
end
if @user.update(user_params) if @user.update(user_params)
redirect_to admin_users_path, notice: "User updated successfully." redirect_to admin_users_path, notice: "User updated successfully."
else else
@@ -40,6 +46,13 @@ class Admin::UsersController < Admin::BaseController
end end
def user_params def user_params
params.require(:user).permit(:name, :email, :role, :primary_language) permitted = params.require(:user).permit(:name, :email, :primary_language)
# Only allow role if it's a valid role enum value
if params[:user][:role].present? && User.roles.key?(params[:user][:role])
permitted[:role] = params[:user][:role]
end
permitted
end end
end end
+7 -1
View File
@@ -2,7 +2,7 @@ class EntriesController < ApplicationController
before_action :set_entry, only: [ :show, :edit, :update ] before_action :set_entry, only: [ :show, :edit, :update ]
def index def index
@language_code = params[:language].presence @language_code = validate_language_code(params[:language].presence)
@category = params[:category].presence @category = params[:category].presence
@query = params[:q].to_s.strip @query = params[:q].to_s.strip
@starts_with = params[:starts_with].presence @starts_with = params[:starts_with].presence
@@ -79,4 +79,10 @@ class EntriesController < ApplicationController
def entry_params def entry_params
params.require(:entry).permit(:category) params.require(:entry).permit(:category)
end end
def validate_language_code(code)
return nil if code.blank?
SupportedLanguage.valid_codes.include?(code) ? code : nil
end
end end
+1 -1
View File
@@ -35,7 +35,7 @@ class Entry < ApplicationRecord
return none unless valid_lang?(language_code) return none unless valid_lang?(language_code)
where.not(language_code => [ nil, "" ]) where.not(language_code => [ nil, "" ])
.order(Arel.sql("#{language_code} ASC")) .order(arel_table[language_code].asc)
end end
private private
+3
View File
@@ -21,6 +21,9 @@ class User < ApplicationRecord
validates :email, presence: true, uniqueness: true validates :email, presence: true, uniqueness: true
validates :password, length: { minimum: 12 }, if: -> { password.present? } validates :password, length: { minimum: 12 }, if: -> { password.present? }
scope :by_role, ->(role) { where(role: role) if role.present? }
scope :search_email, ->(q) { where("email LIKE ?", "%#{sanitize_sql_like(q)}%") if q.present? }
# Invitation token expires after 14 days # Invitation token expires after 14 days
INVITATION_TOKEN_EXPIRY = 14.days INVITATION_TOKEN_EXPIRY = 14.days
+20 -11
View File
@@ -22,6 +22,24 @@
- [ ] Contributor permissions enforcement (for entry editing) - [ ] Contributor permissions enforcement (for entry editing)
- [ ] Reviewer permissions enforcement (for review queue) - [ ] Reviewer permissions enforcement (for review queue)
## Security & Vulnerabilities
- [x] **Fixed user-controlled method execution** (HIGH)
- Added language code validation in EntriesController
- Prevents arbitrary method execution via `public_send()`
- [x] **Fixed SQL injection in Entry model** (MEDIUM)
- Replaced string interpolation with Arel safe column references
- Changed `Arel.sql("#{language_code} ASC")` to `arel_table[language_code].asc`
- [x] **Fixed mass assignment vulnerabilities** (MEDIUM)
- Added role validation in admin invitations and user management
- Only allows valid enum role values
- Prevents users from modifying their own role
- [x] **Fixed SQL LIKE injection** (MEDIUM)
- Added `sanitize_sql_like()` for email search in UsersController
- Prevents wildcard injection attacks
**Status:** All Brakeman security warnings resolved ✓
## Core Features ## Core Features
### Search & Browse ### Search & Browse
@@ -119,24 +137,15 @@
--- ---
## Completed ## Completed (Not Tracked Above)
- [x] **Invitation system** (complete flow with email, acceptance, and expiry validation)
- [x] **Invitation acceptance flow** (users can accept invitations and set passwords)
- [x] **Invitation mailer** (HTML and text email templates with styled design)
- [x] **Token expiry validation** (14-day expiration for invitation links)
- [x] **Controller tests** (40 tests with 160+ assertions for authentication)
- [x] **Authentication system** (login/logout with session management)
- [x] **Admin layout design** updated to match entries page style - [x] **Admin layout design** updated to match entries page style
- [x] **Dynamic navigation** (Admin button for logged-in admins, Sign In for guests) - [x] **Dynamic navigation** (Admin button for logged-in admins, Sign In for guests)
- [x] **Authorization middleware** (Admin::BaseController with role checks) - [x] **Controller tests** (40 tests with 160+ assertions for authentication)
- [x] **Invitation token generation** (secure token creation for new users)
- [x] **Search input loses focus on filter change** - [x] **Search input loses focus on filter change**
- [x] **Mismatched enum syntax** in models - [x] **Mismatched enum syntax** in models
- [x] **Replace hardcoded LANGUAGE_COLUMNS** with dynamic query - [x] **Replace hardcoded LANGUAGE_COLUMNS** with dynamic query
- [x] **Improve fixture quality** (resolved foreign key violations) - [x] **Improve fixture quality** (resolved foreign key violations)
- [x] **XLSX download** button for entries
- [x] **FTS5 integration** (migration added)
- [x] **Database schema** implementation (all models and migrations) - [x] **Database schema** implementation (all models and migrations)
- [x] **Supported languages** table with seed data - [x] **Supported languages** table with seed data
- [x] **Filters do not update with new search results** - [x] **Filters do not update with new search results**