Skip to content

Conversation

@cktricky
Copy link
Contributor

@cktricky cktricky commented Dec 4, 2025

This pull request introduces several significant updates to modernize the Rails application and add security-focused demonstration endpoints. The most impactful changes are the upgrade to Rails 8 and Ruby 3.3.6, the addition of endpoints demonstrating Regular Expression Denial of Service (ReDoS) vulnerabilities and supply chain security issues, and the implementation of Rails 8's regular expression timeout protection. Additionally, the Gemfile has been updated to reflect modern dependencies, and the application layout now includes an example of a supply chain vulnerability.

Framework and Dependency Upgrades:

  • Upgraded Ruby version to 3.3.6 (.ruby-version) and Rails to ~> 8.0.0, with corresponding updates to the Gemfile and modernized dependencies (e.g., puma, sqlite3, asset pipeline gems). Removed deprecated gems and added support for modern asset management (importmap-rails, stimulus-rails, turbo-rails). [1] [2] [3]

Security Demonstration Endpoints (TutorialsController):

  • Added endpoints to demonstrate ReDoS vulnerabilities (redos_email, redos_username), a safe regex validation (redos_email_safe), and supply chain security issues (supply_chain, check_dependencies). These endpoints are documented for security training purposes and reference mitigation strategies. [1] [2]

ReDoS Protection (Rails 8 Feature):

  • Enabled Rails 8's global regular expression timeout (Regexp.timeout = 1.0) to mitigate ReDoS attacks, with a new initializer explaining this security feature.

Supply Chain Vulnerability Demonstration:

  • Updated application.html.erb to include CDN assets without Subresource Integrity (SRI) as a live example of a supply chain vulnerability, with documentation and references to the new tutorial endpoint.

Miscellaneous:

  • Migrated legacy CoffeeScript asset (password_resets.js.coffee) to plain JavaScript (password_resets.js). [1] [2]
  • Updated db/schema.rb to match Rails 8 conventions and schema format. [1] [2]

These changes collectively modernize the codebase, improve security posture, and provide hands-on examples for security education.

cktricky and others added 2 commits December 4, 2025 15:30
This major upgrade brings RailsGoat up to date with the latest versions:
- Ruby 2.6.5 → 3.3.6
- Rails 6.0.0 → 8.0.4

## Key Changes

### Dependencies
- Upgraded all gems to Rails 8-compatible versions
- Removed deprecated gems: therubyracer, coffee-rails, poltergeist,
  travis-lint, rails-perftest, unicorn, powder, rubocop-github
- Updated puma to 6.6.1, sqlite3 to 2.8.1, rspec-rails to 8.0.2
- Added modern Rails 8 features: importmap-rails, stimulus-rails, turbo-rails
- Replaced poltergeist with selenium-webdriver for integration tests

### Code Changes
- Converted CoffeeScript files to plain JavaScript
- Updated test configuration to use Selenium headless driver
- Updated database schema to Rails 8 format

## Testing
- Application starts successfully and responds to requests
- Test suite runs with 23 examples (14 intentional vulnerability failures)
- Database migrations applied successfully

## Notes
This upgrade maintains all intentional security vulnerabilities that make
RailsGoat an effective training tool. The failing tests are expected and
demonstrate the vulnerabilities the application is designed to teach.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
This commit adds comprehensive coverage of OWASP Top 10 2025 categories,
implementing both ReDoS (A05:2025) and Software Supply Chain (A03:2025)
vulnerabilities for educational purposes.

## New Vulnerabilities Added

### A05:2025 - Injection (ReDoS)
- Implemented three ReDoS endpoints in TutorialsController:
  - POST /tutorials/redos_email - Vulnerable email regex with nested quantifiers
  - POST /tutorials/redos_username - Classic (a+)+ pattern
  - POST /tutorials/redos_email_safe - Secure version using URI::MailTo::EMAIL_REGEXP
- Added Regexp.timeout = 1.0 configuration (Rails 8 protection)
- All endpoints include timing and error handling demonstrations

### A03:2025 - Software Supply Chain Failures
- Demonstrated missing SRI on CDN assets in application.html.erb
- Added educational endpoints:
  - GET /tutorials/supply_chain - Comprehensive supply chain vulnerabilities overview
  - GET /tutorials/check_dependencies - Dependency scanning simulation
- Covers: Missing SRI, outdated dependencies, no SBOM, insecure gem sources

## Files Changed

### New Files
- config/initializers/regexp_timeout.rb: Enables Rails 8 ReDoS protection
- spec/controllers/tutorials_controller_spec.rb: 23 passing tests for all endpoints

### Modified Files
- app/controllers/tutorials_controller.rb: Added 5 new educational endpoints
- app/views/layouts/application.html.erb: Added CDN assets WITHOUT SRI (intentional vuln)
- config/routes.rb: Added routes for ReDoS and supply chain endpoints

## Test Coverage
- 23 RSpec tests covering both ReDoS and A03 vulnerabilities
- Tests validate vulnerability behavior, error handling, and educational content
- All tests passing

## Educational Value
- Demonstrates OWASP 2025 categories A03 and A05
- Shows both vulnerable and secure implementations
- Includes real-world CVE examples (British Airways, Magecart)
- Provides mitigation guidance and tool recommendations

This completes 100% coverage of OWASP Top 10 2025 categories in RailsGoat Rails 8.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@dryrunsecurity
Copy link

dryrunsecurity bot commented Dec 6, 2025

DryRun Security

🟡 Please give this pull request extra attention during review.

This pull request introduces multiple security vulnerabilities including ReDoS-prone regular expressions, hardcoded credentials, unsafe deserialization via Marshal.load of user-controlled data, path traversal and unrestricted file uploads, numerous IDORs and an endpoint that decrypts data with hardcoded keys, and many confirmed or potential XSS issues from unescaped/raw outputs and unsafe client-side DOM writes. Together these findings expose risks of denial-of-service, remote code execution, sensitive data disclosure, unauthorized data access/modification, and cross-site scripting.

🟡 Potential Cross-Site Scripting in app/views/layouts/shared/_header.html.erb
Vulnerability Potential Cross-Site Scripting
Description The code calls current_user.first_name.html_safe in a view, which disables Rails' automatic HTML escaping and directly injects user-controlled data into the rendered HTML. If an attacker can control the first_name attribute (or it contains unexpected content), they can inject arbitrary HTML/JavaScript into the page (reflected/stored XSS). No contextual sanitization or safe encoding is applied before rendering.

<span class="text-dark"><%= current_user.first_name.html_safe %></span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>

🟡 Potential Cross-Site Scripting in app/views/layouts/shared/_messages.html.erb
Vulnerability Potential Cross-Site Scripting
Description The template renders flash messages with a raw ERB expression (<%= msg %>) inside an HTML context. In Rails ERB, <%= %> normally HTML-escapes output unless the string has been marked html_safe. However, the analysis cannot assume all flash messages are application-only: if any code elsewhere assigns user-controlled input to the flash (e.g., flash[:notice] = params[:something], or copies user content into a flash entry), that user data will be rendered here. Because this view does not explicitly sanitize or escape msg and does not defend against flash values marked html_safe, a path exists for attacker-controlled content to be sent to the rendering sink (HTML body) and executed. Therefore this is a valid reflected/stored XSS risk contingent on flash contents being derived from untrusted input.

<%= msg %>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>

🟡 Potential Cross-Site Scripting in app/views/benefit_forms/index.html.erb
Vulnerability Potential Cross-Site Scripting
Description The file input change handler takes the raw filename (from $(this).val()), extracts parts client-side, and inserts it into the page via jQuery .html() without escaping. The code: $(".filename").html(' ' + fileName); directly concatenates user-controllable data into HTML, allowing injection of HTML/script if an attacker can supply a crafted fileName value (for example via a manipulated client, browser extension, or other vector). Using .text() or properly escaping the filename before inserting it would be required to prevent XSS.

$(".filename").html('<i class="bi bi-file-earmark-check-fill text-success"></i> ' + fileName);
// Highlight the upload area
$(".upload-area").css({

🟡 Potential Cross-Site Scripting in app/views/admin/dashboard.html.erb
Vulnerability Potential Cross-Site Scripting
Description The template directly injects params[:admin_id] into a JavaScript string concatenation used to build a URL passed to jQuery's .load(). The line: $("#userDataTable").load("/admin/" + <%= params[:admin_id] %> + "/get_all_users"); interpolates an unescaped server-side value into client-side code. If an attacker can control params[:admin_id], they can break out of the JavaScript context or produce an unexpected URL that returns attacker-controlled HTML/JS which will be inserted and executed by .load(). There is no sanitization or explicit numeric coercion shown, nor escaping to ensure the value is safe, so this is a plausible reflected/stored XSS/vector for script injection.

$("#userDataTable").load("/admin/" + <%= params[:admin_id] %> + "/get_all_users");
}
$(document).ready(function() {

🟡 Potential Cross-Site Scripting in app/views/layouts/application.html.erb
Vulnerability Potential Cross-Site Scripting
Description The layout template injects the value of cookies[:font] directly into a <style> block using raw (ERB raw interpolation) without any escaping or validation: body { font-size:<%= raw cookies[:font] %> !important;}. This is user-controllable data (cookies) written into an HTML context (inside a <style> tag). An attacker who can set a cookie can close the CSS context and inject HTML/JS (for example by supplying a value like "12px}</style><script>alert(1)</script><style>{"), or craft CSS that triggers script execution in some browsers). Rails automatic escaping for templates does not apply here because raw is used, and there is no sanitization or validation of the cookie value. Therefore this is a confirmed server-side XSS/vector for injection.

body { font-size:<%= raw cookies[:font] %> !important;}
<%
end
%>

🟡 Potential Cross-Site Scripting in app/views/messages/index.html.erb
Vulnerability Potential Cross-Site Scripting
Description Multiple unescaped user-controlled values are rendered into HTML and JavaScript contexts and some explicit unsafe APIs are used. In the messages view, message.message and message.creator_name are output using <%= ... %> which in Rails is auto-escaped for HTML body contexts — but elsewhere in the PR there are clear explicit bypasses of escaping and direct DOM insertion of user input (document.write in app/views/sessions/new.html.erb) and uses of html_safe/inspect.html_safe in other templates. The PR also constructs a JS URL using <%= "/users/#{current_user.id}/messages.json".inspect.html_safe %> which uses html_safe/inspect and may allow injection if other parts use similar patterns with user data. The document.write of a decoded location.hash is a direct, high-confidence reflected XSS vector. Given the combination of direct DOM writes and explicit .html_safe usage in the same patchset, user-controlled message content and user names could be abused (especially if any of them are ever rendered with html_safe/raw or injected into JS).

<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-envelope-fill text-primary"></i> Messages
</h2>
<p class="text-muted">Inbox for <%= current_user.full_name %></p>
</div>
</div>
<div class="row g-3">
<!-- Messages Inbox -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-inbox text-primary"></i> Inbox
</h4>
<p class="text-muted mb-0 small mt-1">Your received messages</p>
</div>
<div class="card-body p-0">
<% if @messages.any? %>
<div class="messages-list">
<% @messages.each do |message| %>
<div class="message-item">
<div class="message-avatar">
<div class="avatar-circle">
<i class="bi bi-person-fill"></i>
</div>
</div>
<div class="message-content">
<div class="message-header">
<div class="message-from">
<strong><%= message.creator_name %></strong>
</div>
<div class="message-date">
<i class="bi bi-calendar3 me-1"></i>
<%= message.created_at.strftime("%b %d, %Y") %>
</div>
</div>
<div class="message-text">
<%= message.message %>
</div>
<div class="message-actions">
<%= link_to user_message_path(:id => message.id), class: "btn btn-sm btn-outline-primary" do %>
<i class="bi bi-eye"></i> Details
<% end %>
<%= link_to user_message_path(:id => message.id), method: 'delete', data: { confirm: 'Are you sure?' }, class: "btn btn-sm btn-outline-danger" do %>
<i class="bi bi-trash"></i> Delete
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="empty-state">
<i class="bi bi-inbox"></i>
<h5>No Messages Yet</h5>
<p class="text-muted">Your inbox is empty. Send a message to get started!</p>
</div>
<% end %>
</div>
</div>
</div>
<!-- Send Message Form -->
<div class="col-lg-4">
<div class="card shadow-sm sticky-top" style="top: 80px; border-left: 4px solid var(--rg-success);">
<div class="card-header py-3" style="background: linear-gradient(135deg, rgba(6, 214, 160, 0.05), rgba(30, 130, 94, 0.05));">
<h4 class="mb-0">
<i class="bi bi-send text-success"></i> Send Message
</h4>
<p class="text-muted mb-0 small mt-1">Compose a new message</p>
</div>
<div class="card-body p-4">
<!-- Alert Messages -->
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>Success!</strong>
<p class="mb-0 small">Message sent successfully.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<strong>Error!</strong>
<p class="mb-0 small">Failed to send message.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<%= form_for @message, url: user_messages_path, method: :post, html: { id: "send_message" } do |f| %>
<%= f.hidden_field :creator_id, value: current_user.id %>
<%= f.hidden_field :read, value: '0' %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-person-circle text-success me-2"></i>To
</label>
<%= f.select(:receiver_id,
options_from_collection_for_select(User.all, :id, :full_name),
{},
{ class: "form-select form-select-lg" }) %>
<small class="text-muted">Select message recipient</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-chat-left-text text-success me-2"></i>Message
</label>
<%= f.text_area :message,
class: "form-control form-control-lg",
rows: 6,
placeholder: "Type your message here...",
style: "resize: vertical;" %>
<small class="text-muted">Write your message content</small>
</div>
<div class="d-grid">
<%= f.submit "Send Message",
id: 'submit_button',
class: "btn btn-success btn-lg" %>
</div>
<div class="mt-3 p-3 rounded" style="background: var(--rg-light); border-left: 3px solid var(--rg-success);">
<small class="text-muted">
<i class="bi bi-info-circle-fill text-success me-1"></i>
<strong>Tip:</strong> Messages are delivered instantly
</small>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive(){
$('li[id="messages"]').addClass('active');
}
$(document).ready(function() {
makeActive();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
// Form submission with AJAX
$("#submit_button").click(function(event) {
event.preventDefault();
var valuesToSubmit = $("#send_message").serialize();
$.ajax({
url: <%= "/users/#{current_user.id}/messages.json".inspect.html_safe %>,
data: valuesToSubmit,
type: "POST",
success: function(response) {
if (response.msg == "failure") {
$('#failure').show(500).delay(2000).fadeOut();
} else {
$('#success').show(500).delay(2000).fadeOut();
// Clear form on success
$('#send_message')[0].reset();
// Reload page after delay to show new message
setTimeout(function() {
location.reload();
}, 2500);
}
},
error: function(event) {
$('#failure').show(500).delay(2000).fadeOut();
}
});
});
</script>
<style>
/* Messages List Styling */
.messages-list {
display: flex;
flex-direction: column;
}
.message-item {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-bottom: 1px solid #e9ecef;
transition: background-color 0.2s ease;
}
.message-item:hover {
background-color: rgba(230, 57, 70, 0.03);
}
.message-item:last-child {
border-bottom: none;
}
.message-avatar {
flex-shrink: 0;
}
.avatar-circle {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, var(--rg-primary) 0%, var(--rg-primary-dark) 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
box-shadow: 0 2px 8px rgba(230, 57, 70, 0.2);
}
.message-content {
flex: 1;
min-width: 0;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.message-from {
font-size: 1.1rem;
color: var(--rg-dark);
}
.message-date {
font-size: 0.875rem;
color: #6c757d;
white-space: nowrap;
}
.message-text {
color: #495057;
margin-bottom: 1rem;
line-height: 1.6;
word-wrap: break-word;
}
.message-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #6c757d;
}
.empty-state i {
font-size: 4rem;
opacity: 0.3;
margin-bottom: 1rem;
}
.empty-state h5 {
margin-bottom: 0.5rem;
color: #495057;
}
/* Sticky Form */
@media (min-width: 992px) {
.sticky-top {
position: sticky;
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.message-item {
flex-direction: column;
text-align: center;
}
.message-header {
flex-direction: column;
text-align: center;
}
.message-actions {
justify-content: center;
}
}
</style>

🟡 Potential Cross-Site Scripting in app/views/paid_time_off/index.html.erb
Vulnerability Potential Cross-Site Scripting
Description There are multiple places where untrusted or developer-controlled values are emitted into HTML or JavaScript without proper escaping or with explicit bypasses of Rails escaping. Notable confirmed unsafe patterns: 1) Use of html_safe/raw on user-visible data: current_user.first_name.html_safe in app/views/layouts/shared/_header.html.erb directly outputs a user-controlled string into HTML without escaping — this is a classic stored/reflected XSS vector. 2) Direct document.write of location.hash content in app/views/sessions/new.html.erb: document.write("
" + paramValue + "
"); writes decoded URL hash data directly into the page DOM without encoding, enabling XSS via crafted URL fragments. 3) In multiple views JavaScript embeds server-side values via .inspect.html_safe or raw cookies: examples include get_pto_schedule_schedule_index_path(...).inspect.html_safe used inside JS and body { font-size:<%= raw cookies[:font] %> !important;} in app/views/layouts/application.html.erb — both can lead to script/HTML injection if the embedded value is attacker-controllable. Combined with direct interpolation of model attributes into HTML (e.g. <%= message.message %>, <%= @pto.* %>, many form field values) and several places where helpers are used with .html_safe or raw, there exists a clear data flow from potentially attacker-controlled sources (user attributes, URL hash, cookies, model data) to rendering sinks (HTML body, innerHTML/document.write, inline script) without context-appropriate escaping or sanitization. According to framework escaping rules, plain <%= %> would normally be escaped, but explicit uses of html_safe/raw and direct DOM write bypass escaping; additionally embedding values into JavaScript via .inspect.html_safe is unsafe. Therefore XSS is confirmed.

<div class="container-fluid">
<!-- Alert Messages -->
<div class="row">
<div class="col-12">
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">Information successfully updated.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Failed to update. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<!-- Calendar and Schedule Form Row -->
<div class="row g-3">
<!-- PTO Calendar -->
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-calendar3 text-primary"></i> PTO Calendar
</h4>
</div>
<div id="calendarDiv" class="card-body">
<div id='calendar'></div>
</div>
</div>
</div>
<!-- Schedule PTO Form -->
<div class="col-lg-6">
<div class="card shadow-sm" style="border-left: 4px solid var(--rg-primary);">
<div class="card-header py-3" style="background: linear-gradient(135deg, rgba(230, 57, 70, 0.05), rgba(214, 40, 40, 0.05));">
<h4 class="mb-0">
<i class="bi bi-calendar-plus text-primary"></i> Schedule PTO
</h4>
<p class="text-muted mb-0 small mt-1">Plan your time away from work</p>
</div>
<div id="scheduleDiv" class="card-body p-4">
<%= form_for @schedule, url: "#", html: { id: "cal_update" } do |s| %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-tag-fill text-primary me-2"></i>Event Name
</label>
<%= s.text_field :event_name, {
placeholder: "e.g., Summer Vacation, Personal Day",
class: "form-control form-control-lg"
} %>
</div>
<%= s.text_field :event_type, type: "hidden", value: "pto" %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-chat-left-text-fill text-primary me-2"></i>Event Description
</label>
<%= s.text_field :event_desc, {
placeholder: "e.g., Family trip to Hawaii, Medical appointment",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Optional: Add details about your time off</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold" for="date_range1">
<i class="bi bi-calendar-event-fill text-primary me-2"></i>Event Dates
</label>
<div class="input-group input-group-lg">
<span class="input-group-text bg-white">
<i class="bi bi-calendar-range text-primary"></i>
</span>
<input type="text" name="date_range1" id="date_range1" class="form-control date_picker" placeholder="Click to select date range"/>
</div>
<small class="text-muted">Choose the start and end dates for your PTO</small>
</div>
<div class="d-grid">
<%= s.submit "Schedule PTO", {
id: 'cal_update_submit',
class: "btn btn-primary btn-lg"
} %>
</div>
<div class="mt-3 p-3 rounded" style="background: var(--rg-light); border-left: 3px solid var(--rg-success);">
<small class="text-muted">
<i class="bi bi-info-circle-fill text-primary me-1"></i>
<strong>Tip:</strong> Your PTO request will appear on the calendar after submission
</small>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Sick Days Stats -->
<div class="row mt-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-bandaid text-primary"></i> Sick Days
</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #579da9;">
<div class="card-body">
<div class="text-muted small mb-1">Days Earned</div>
<h3 class="mb-0" style="color: #579da9;"><%= @pto.sick_days_earned %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #e26666;">
<div class="card-body">
<div class="text-muted small mb-1">Days Taken</div>
<h3 class="mb-0" style="color: #e26666;"><%= @pto.sick_days_taken %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #1e825e;">
<div class="card-body">
<div class="text-muted small mb-1">Days Remaining</div>
<h3 class="mb-0" style="color: #1e825e;"><%= @pto.sick_days_remaining %></h3>
</div>
</div>
</div>
</div>
<div class="text-center text-muted mt-3 small">
<i class="bi bi-info-circle"></i> As of today: <%= Date.today.strftime("%B %d, %Y") %>
</div>
</div>
</div>
</div>
</div>
<!-- PTO Stats -->
<div class="row mt-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-umbrella-fill text-primary"></i> Paid Time Off
</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #579da9;">
<div class="card-body">
<div class="text-muted small mb-1">Days Earned</div>
<h3 class="mb-0" style="color: #579da9;"><%= @pto.pto_earned %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #e26666;">
<div class="card-body">
<div class="text-muted small mb-1">Days Taken</div>
<h3 class="mb-0" style="color: #e26666;"><%= @pto.pto_taken %></h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center" style="border-left: 4px solid #1e825e;">
<div class="card-body">
<div class="text-muted small mb-1">Days Remaining</div>
<h3 class="mb-0" style="color: #1e825e;"><%= @pto.pto_days_remaining %></h3>
</div>
</div>
</div>
</div>
<div class="text-center text-muted mt-3 small">
<i class="bi bi-info-circle"></i> As of today: <%= Date.today.strftime("%B %d, %Y") %>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function makeActive() {
$('li[id="pto"]').addClass('active');
}
$(document).ready(function() {
makeActive();
// Initialize FullCalendar
$('#calendar').fullCalendar({
events: <%= get_pto_schedule_schedule_index_path(:format => "json").inspect.html_safe %>,
height: 'auto',
contentHeight: 'auto',
aspectRatio: 1.5
});
// Initialize date range picker
$('.date_picker').daterangepicker({
opens: 'right',
locale: {
format: 'MM/DD/YYYY'
}
});
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
makeActive();
});
// Form submission
$("#cal_update_submit").click(function(event) {
event.preventDefault();
var valuesToSubmit = $("#cal_update").serialize();
$.ajax({
url: "/schedule.json",
data: valuesToSubmit,
type: "POST",
success: function(response) {
if (response.msg == "failure") {
$('#failure').show(500).delay(1500).fadeOut();
} else {
$('#success').show(500).delay(1500).fadeOut();
$('#calendar').fullCalendar('refetchEvents');
// Clear form
$('#cal_update')[0].reset();
}
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
</script>
<style>
/* FullCalendar modern styling */
#calendar {
border-radius: 0.5rem;
}
.fc-toolbar {
background: var(--rg-light);
padding: 1rem;
border-radius: 0.5rem 0.5rem 0 0;
}
.fc-button {
background: var(--rg-primary) !important;
border-color: var(--rg-primary) !important;
border-radius: 0.5rem !important;
text-transform: none !important;
padding: 0.375rem 0.75rem !important;
}
.fc-button:hover {
background: var(--rg-primary-dark) !important;
border-color: var(--rg-primary-dark) !important;
}
.fc-day-header {
background: var(--rg-light);
padding: 0.75rem;
font-weight: 600;
}
.fc-event {
background: var(--rg-primary);
border-color: var(--rg-primary);
border-radius: 0.375rem;
padding: 0.25rem 0.5rem;
}
.fc-today {
background: rgba(230, 57, 70, 0.05) !important;
}
</style>

🟡 Potential Cross-Site Scripting in app/views/pay/index.html.erb
Vulnerability Potential Cross-Site Scripting
Description The JavaScript function parseDirectDepostInfo injects server-provided values directly into HTML strings concatenated into table cells: '' + val.bank_account_num + '' and similar for routing number and percent. DataTables then inserts these strings into the DOM without any escaping. If any of these fields (bank_account_num, bank_routing_num, percent_of_deposit) can contain attacker-controlled input (from the database or API), an attacker can supply HTML/JS payloads (e.g., <script> or event handlers) which will be rendered/executed in the user's browser — this is a stored/reflected XSS vector. No client-side escaping or sanitization is applied before insertion, and the server-side endpoint is used via AJAX and its JSON is parsed and trusted, so the injection path is present.

table.row.add( [
'<code class="text-monospace">' + val.bank_account_num + '</code>',
'<span class="badge bg-light text-dark">' + val.bank_routing_num + '</span>',
'<span class="badge bg-success">' + val.percent_of_deposit + '%</span>',
buildDeleteLink(val.id)
] );

🟡 Potential Cross-Site Scripting in app/views/pay/index.html.erb
Vulnerability Potential Cross-Site Scripting
Description The decryptShow function constructs an HTML string by concatenating msg.account_num directly into the markup and appends it to the DOM via $('body').append(alertHtml). msg.account_num comes from a JSON response (server-controlled) and is inserted without any encoding/escaping. This allows an attacker who can control the decrypted value (or influence the server response) to inject arbitrary HTML/JS (for example a <script> tag or on* attributes), resulting in DOM-based XSS when appended to the page.

'<p class="mb-0"><strong style="font-size: 1.2rem; font-family: monospace;">' + msg.account_num + '</strong></p>' +
'</div>' +
'</div>' +
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' +

🟡 Potential Cross-Site Scripting in app/views/sessions/new.html.erb
Vulnerability Potential Cross-Site Scripting
Description The login view reads attacker-controllable data from location.hash, decodes it, and injects it directly into the page via document.write without any escaping or sanitization. This is a DOM-based XSS sink: document.write will interpret HTML/JS, so a crafted URL like https://example/login#msg=<script>alert(1)</script> (properly encoded) will execute script in the victim's browser. There is no framework auto-escaping involved because the data is handled entirely in client-side JS and written into the DOM raw.

document.write("<div class='alert alert-info mt-3'>" + paramValue + "</div>");
}
} catch(err) {
// Silently fail

🟡 Potential Cross-Site Scripting in app/views/performance/index.html.erb
Vulnerability Potential Cross-Site Scripting
Description Multiple template expressions output untrusted model fields directly into HTML content and into attribute/context-sensitive spots without any explicit escaping or sanitization. In ERB, <%= ... %> is escaped by default, but there are several risky contexts here: (1) p.comments and p.reviewer_name are rendered inside element content (<%= p.comments %>, <%= p.reviewer_name %>) — if these fields are HTML-safe elsewhere (e.g. marked html_safe in model/controller) or contain untrusted markup they could introduce XSS. (2) Values are interpolated into inline style attributes (style="background-color: <%= score_color %>" and style="width: <%= score_percentage %>%"). Although score_color and score_percentage in this code are derived from p.score and numeric calculations, if an attacker can influence p.score (or if types are not enforced) they may inject a value that breaks out of the style context. (3) aria/role/other attributes concatenating values (aria-valuenow="<%= p.score %>") could also be abused if non-numeric content is present. Given the codebase already contains other explicit unsafe usages (e.g. current_user.first_name.html_safe and document.write of hash param elsewhere), there is a realistic risk that model fields are ever marked safe or set from attacker-controlled input. Taken together, these patterns provide a feasible path for untrusted data to reach rendered HTML without robust contextual encoding or sanitization, so XSS is confirmed.

<div class="progress-bar" role="progressbar"
style="width: <%= score_percentage %>%; background-color: <%= score_color %>; font-weight: 600; font-size: 1rem;"
aria-valuenow="<%= p.score %>" aria-valuemin="0" aria-valuemax="5">
<%= p.score %> / 5 - <%= p.comments %>

Regular Expression Denial of Service (ReDoS) in app/controllers/tutorials_controller.rb
Vulnerability Regular Expression Denial of Service (ReDoS)
Description The redos_email method uses a complex regular expression (email_pattern) to validate email addresses. This regex contains nested quantifiers (+ applied to [a-zA-Z0-9_\-\.]+ and + applied to ([a-zA-Z0-9\-]+\.)+) and overlapping character classes, which are classic 'evil regex' patterns susceptible to catastrophic backtracking. Although a Regexp.timeout of 1 second is configured, a malicious input can still consume significant CPU resources for the entire duration of the timeout, leading to a denial of service by exhausting server resources.

email_pattern = /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/
begin
start_time = Time.now

Regular Expression Denial of Service (ReDoS) in app/controllers/tutorials_controller.rb
Vulnerability Regular Expression Denial of Service (ReDoS)
Description The redos_username method in app/controllers/tutorials_controller.rb uses the regular expression /^(a+)+$/ to validate a username. This regex contains nested quantifiers (+ inside +) applied to a repeating character (a), which is a classic pattern for Regular Expression Denial of Service (ReDoS). User input from params[:username] is directly passed to this regex. A specially crafted input string, such as a long sequence of 'a' characters followed by a non-matching character (e.g., 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa!'), will cause the regex engine to engage in catastrophic backtracking, consuming excessive CPU resources and potentially leading to a denial of service. Although Regexp.timeout is set to 1 second in config/initializers/regexp_timeout.rb, this only prevents an infinite hang; it does not prevent the server from expending significant resources for each malicious request, making it vulnerable to resource exhaustion.

username_pattern = /^(a+)+$/
begin
start_time = Time.now

Hardcoded Credentials in app/views/tutorials/credentials.html.erb
Vulnerability Hardcoded Credentials
Description The file app/views/tutorials/credentials.html.erb explicitly contains a table with hardcoded user emails, passwords, and API keys. This information is directly embedded in the HTML, making it accessible to any user who can view this page within the application. Although this is an intentional vulnerability for a tutorial, it represents a critical information disclosure risk in a real-world application.

<td style="word-wrap:break-word;">[email protected]</td>
<td>admin1234</td>
<td>1-01de24d75cffaa66db205278d1cf900bf087a737</td>
</tr>

Unsafe Deserialization in app/views/password_resets/reset_password.html.erb
Vulnerability Unsafe Deserialization
Description The application uses Marshal.load(Base64.decode64(params[:user])) in the reset_password action of the PasswordResetsController. The params[:user] value is derived directly from a hidden field in the reset_password.html.erb view, which is populated with a Base64-encoded, marshaled Ruby object. This allows an attacker to craft a malicious serialized object, Base64 encode it, and send it as the user parameter. When Marshal.load attempts to deserialize this attacker-controlled data, it can lead to arbitrary code execution on the server.

<%= hidden_field_tag 'user', Base64.encode64(Marshal.dump(@user)) %>
<div class="mb-3">
<label for="password" class="form-label">New Password</label>

Path Traversal in app/views/benefit_forms/index.html.erb
Vulnerability Path Traversal
Description The download action in BenefitFormsController directly uses the name parameter from user input to construct a file path without any sanitization or validation. This allows an attacker to use directory traversal sequences (../) to access and download arbitrary files from the server's filesystem.

<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-file-earmark-medical text-primary"></i> Benefit Forms
</h2>
<p class="text-muted">Download benefit documents and upload completed forms</p>
</div>
</div>
<!-- Download Forms Section -->
<div class="row g-3 mb-4">
<!-- Health Insurance Card -->
<div class="col-lg-6">
<div class="card shadow-sm h-100 hover-card">
<div class="card-body text-center p-4">
<div class="mb-3">
<i class="bi bi-heart-pulse-fill" style="font-size: 3rem; color: var(--rg-primary);"></i>
</div>
<h4 class="card-title mb-3">Health Insurance</h4>
<p class="text-muted mb-4">Download your health insurance benefit forms and information</p>
<%= link_to download_path(type: "File", name: "public/docs/Health_n_Stuff.pdf"), class: "btn btn-primary btn-lg" do %>
<i class="bi bi-file-earmark-pdf"></i> Download PDF
<% end %>
</div>
</div>
</div>
<!-- Dental Insurance Card -->
<div class="col-lg-6">
<div class="card shadow-sm h-100 hover-card">
<div class="card-body text-center p-4">
<div class="mb-3">
<i class="bi bi-emoji-smile-fill" style="font-size: 3rem; color: var(--rg-success);"></i>
</div>
<h4 class="card-title mb-3">Dental Insurance</h4>
<p class="text-muted mb-4">Download your dental insurance benefit forms and information</p>
<%= link_to download_path(type: "File", name: "public/docs/Dental_n_Stuff.pdf"), class: "btn btn-success btn-lg" do %>
<i class="bi bi-file-earmark-pdf"></i> Download PDF
<% end %>
</div>
</div>
</div>
</div>
<!-- Upload Section -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h4 class="mb-0">
<i class="bi bi-cloud-upload text-primary"></i> Upload Completed Forms
</h4>
</div>
<div class="card-body p-4">
<%= form_for @benefits, url: upload_path, html: { multipart: true, id: "fi", class: "needs-validation" } do |f| %>
<%= hidden_field "benefits", "backup", value: false %>
<div class="row g-3">
<div class="col-12">
<div class="upload-area p-4 text-center border rounded" style="border: 2px dashed #dee2e6; background: var(--rg-light); transition: all 0.3s;">
<i class="bi bi-cloud-arrow-up" style="font-size: 3rem; color: var(--rg-secondary);"></i>
<h5 class="mt-3 mb-2">Select File to Upload</h5>
<p class="text-muted mb-3">Choose a file from your computer</p>
<div class="file-input-wrapper">
<label for="benefits_upload" class="btn btn-primary mb-3" style="cursor: pointer;">
<i class="bi bi-folder2-open"></i> Choose File
</label>
<%= f.file_field :upload, class: "d-none", id: "benefits_upload" %>
</div>
<div class="selected-file mt-3">
<span class="filename text-muted">
<i class="bi bi-file-earmark"></i> No file selected
</span>
</div>
</div>
</div>
<div class="col-12">
<div class="d-flex gap-2">
<button id="start_upload" type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-upload"></i> Upload File
</button>
<button type="button" class="btn btn-outline-secondary btn-lg" onclick="document.getElementById('fi').reset(); $('.filename').html('<i class=\'bi bi-file-earmark\'></i> No file selected');">
<i class="bi bi-x-circle"></i> Cancel
</button>
</div>
</div>
<div class="col-12">
<!-- Progress Bar -->
<div class="progress" style="height: 25px; display: none;" id="upload-progress">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" role="progressbar" style="width: 0%;" id="progress-bar">
<span id="progress-text">0%</span>
</div>
</div>
</div>
<div class="col-12">
<!-- Files Table -->
<table class="table table-hover d-none" id="files-table">
<thead class="table-light">
<tr>
<th>File Name</th>
<th>Size</th>
<th>Status</th>
</tr>
</thead>
<tbody class="files" data-toggle="modal-gallery" data-target="#modal-gallery"></tbody>
</table>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Info Box -->
<div class="row mt-4">
<div class="col-12">
<div class="alert alert-info" role="alert">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-2">Important Information</h5>
<ul class="mb-0 ps-3">
<li>Download benefit forms, fill them out completely, and upload them back</li>
<li>Accepted file formats: PDF, DOC, DOCX, JPG, PNG</li>
<li>Maximum file size: 10MB</li>
<li>All uploaded documents are securely stored</li>
</ul>
</div>
</div>
</div>

Unrestricted File Upload in app/views/benefit_forms/index.html.erb
Vulnerability Unrestricted File Upload
Description The application allows users to upload files without proper server-side validation of the file type, content, or filename. The Benefits.save method directly uses the user-provided file.original_filename and stores the file in the public/data directory, which is typically web-accessible. This allows an attacker to upload a malicious script (e.g., a web shell) with an executable extension, which could then be accessed and executed via a web request, leading to Remote Code Execution (RCE).

<%= f.file_field :upload, class: "d-none", id: "benefits_upload" %>
</div>
<div class="selected-file mt-3">

Insecure Direct Object Reference (IDOR) in app/views/messages/index.html.erb
Vulnerability Insecure Direct Object Reference (IDOR)
Description The show and destroy actions in the MessagesController fetch Message objects directly using Message.where(id: params[:id]).first. This allows any authenticated user to view or delete any message in the system by manipulating the id parameter, as there is no check to ensure the message belongs to the current_user or that the current_user has appropriate authorization.

<%= link_to user_message_path(:id => message.id), class: "btn btn-sm btn-outline-primary" do %>
<i class="bi bi-eye"></i> Details
<% end %>
<%= link_to user_message_path(:id => message.id), method: 'delete', data: { confirm: 'Are you sure?' }, class: "btn btn-sm btn-outline-danger" do %>

User Enumeration / Information Disclosure in app/views/messages/index.html.erb
Vulnerability User Enumeration / Information Disclosure
Description The app/views/messages/index.html.erb template uses User.all to populate a recipient dropdown for sending messages. This directly exposes the full names and user IDs of all registered users in the application to any authenticated user, regardless of their role or permissions. This information can be leveraged by attackers for user enumeration, social engineering, or to target specific users in further attacks.

<%= f.select(:receiver_id,
options_from_collection_for_select(User.all, :id, :full_name),
{},
{ class: "form-select form-select-lg" }) %>

Insecure Direct Object Reference (IDOR) in app/views/pay/index.html.erb
Vulnerability Insecure Direct Object Reference (IDOR)
Description The destroy action in the PayController fetches a direct deposit record using Pay.find_by_id(params[:id]). This method directly retrieves the record based on the provided ID from the request parameters without verifying if the record belongs to the currently authenticated user (current_user). This allows an attacker to manipulate the id parameter to delete direct deposit records belonging to other users.

<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-bank text-primary"></i> Direct Deposit & Pay
</h2>
<p class="text-muted">Manage your direct deposit accounts and payment settings</p>
</div>
</div>
<!-- Alert Messages -->
<div class="row">
<div class="col-12">
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">Information successfully updated.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Failed to update. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<div class="row g-4">
<!-- Left Column - Forms -->
<div class="col-lg-5">
<!-- Add Direct Deposit Form -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="bi bi-plus-circle text-success me-2"></i>Add Direct Deposit
</h5>
</div>
<div class="card-body p-4">
<%= form_tag "#", { class: "needs-validation", id: "bank_info_form" } do %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-bank2 text-success me-2"></i>Bank Account Number
</label>
<%= text_field_tag :bank_account_num, params[:bank_account_num], {
placeholder: "Enter account number",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Your bank account number</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-diagram-3 text-success me-2"></i>Bank Routing Number
</label>
<%= text_field_tag :bank_routing_num, params[:bank_routing_num], {
placeholder: "9-digit routing number",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Usually found at the bottom of checks</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-percent text-success me-2"></i>Percentage of Deposit
</label>
<%= text_field_tag :dd_percent, params[:dd_percent], {
placeholder: "e.g., 100",
class: "form-control form-control-lg"
} %>
<small class="text-muted">What percentage to deposit (1-100)</small>
</div>
<div class="d-grid">
<%= submit_tag "Add Account", {
id: "dd_form_btn",
class: "btn btn-success btn-lg"
} %>
</div>
<% end %>
</div>
</div>
<!-- Decrypt Form -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="bi bi-unlock text-warning me-2"></i>Decrypt Account
</h5>
</div>
<div class="card-body p-4">
<%= form_tag "#", { class: "needs-validation", id: "decrypt_form" } do %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-key-fill text-warning me-2"></i>Encrypted Account Number
</label>
<%= text_field_tag :value_to_decrypt, params[:value_to_decrypt], {
placeholder: "Paste encrypted value",
class: "form-control form-control-lg"
} %>
<small class="text-muted">Copy from the table to the right</small>
</div>
<div class="d-grid">
<%= submit_tag "Decrypt", {
id: "decrypt_btn",
class: "btn btn-warning btn-lg"
} %>
</div>
<% end %>
</div>
</div>
</div>
<!-- Right Column - Accounts Table -->
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-list-ul text-primary me-2"></i>Your Accounts
</h5>
<button type="button" class="btn btn-sm btn-outline-secondary" id="encrypted_acct_question">
<i class="bi bi-question-circle me-1"></i> Why Encrypted?
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="data_table">
<thead class="table-light">
<tr>
<th>Account Number</th>
<th>Routing Number</th>
<th>Deposit %</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- DataTable will populate this -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

Information Disclosure via Decryption in app/views/pay/index.html.erb
Vulnerability Information Disclosure via Decryption
Description The application exposes an endpoint (/pay/decrypted_bank_acct_num) that allows any authenticated user to decrypt arbitrary values. The encryption key and Initialization Vector (IV) used for this decryption are hardcoded within the application's codebase (in config/initializers/key.rb for the key in non-production environments, and config/initializers/constants.rb for the IV). This design flaw makes the decryption functionality highly vulnerable to exploitation.

<%= submit_tag "Decrypt", {
id: "decrypt_btn",
class: "btn btn-warning btn-lg"
} %>


All finding details can be found in the DryRun Security Dashboard.

@cktricky
Copy link
Contributor Author

cktricky commented Dec 6, 2025

Holy moly, good job DryRun - shockingly accurate. But those vulns are intentional so we'll let it ride 😎. Thanks for showcasing how dope you are 🤗

cktricky and others added 26 commits December 7, 2025 00:36
Complete UI overhaul bringing RailsGoat into 2024 with a professional,
modern interface while maintaining all security vulnerabilities for
educational purposes.

## Design System
- Modern color palette with CSS variables
- Primary: #e63946 (red), Secondary: #457b9d (blue)
- Professional sans-serif typography
- Consistent spacing and shadows
- Bootstrap Icons for modern iconography
- Responsive design with mobile-first approach

## Layout Changes
- Fixed header with clean navigation (60px height)
- Dark sidebar with modern icons and section headers (250px width)
- Proper spacing and padding throughout
- Responsive breakpoints for mobile/tablet/desktop
- Modern card-based content areas

## Header Modernization
- Clean white header with subtle shadow
- RailsGoat branding with shield icon
- Modern dropdown user menu with avatar
- Improved font size controls
- Better button styling and spacing
- Modal-based credentials display (Bootstrap 5)

## Sidebar Improvements
- Dark navy background (#1d3557)
- Bootstrap Icons instead of custom fonts
- Section headers (Admin, Employee)
- Active state highlighting
- Smooth hover transitions
- Version info in footer

## Login Page Redesign
- Beautiful gradient background
- Centered card with shadow
- Modern form inputs with icons
- Clear call-to-action buttons
- Security training notice banner
- Responsive design

## Components Updated
- Modern alerts with icons and proper dismiss buttons
- Footer with OWASP links and copyright
- Scroll-to-top button (vanilla JS, no jQuery)
- Form controls with proper Bootstrap 5 classes

## Technical Improvements
- Bootstrap 5.3 properly implemented (not just CDN reference)
- Bootstrap Icons 1.11.1 for modern iconography
- Removed jQuery dependencies where possible
- Modern JavaScript (vanilla, no jQuery for new features)
- Proper Bootstrap 5 data attributes (data-bs-*)
- Semantic HTML5 structure

## Security Vulnerabilities Preserved
- XSS via html_safe in user welcome (header)
- XSS via cookie font-size (application layout)
- XSS via URL hash parameter (login page)
- Missing SRI on CDN assets (A03:2025)
- All educational vulnerabilities intact

## Files Modified
- app/views/layouts/application.html.erb - Complete redesign with CSS variables
- app/views/layouts/shared/_header.html.erb - Modern navigation
- app/views/layouts/shared/_sidebar.html.erb - Dark sidebar with icons
- app/views/layouts/shared/_footer.html.erb - Modern footer with links
- app/views/layouts/shared/_messages.html.erb - Bootstrap 5 alerts
- app/views/sessions/new.html.erb - Beautiful login page

This modernization makes RailsGoat visually appealing and professional
while maintaining its core educational purpose. The application now
looks like a modern web app security professionals want to use.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed critical issues causing JavaScript errors on dashboard pages:

## Problems Fixed

1. **jQuery not defined ($)**
   - jQuery was loading AFTER application.js
   - Scripts in dashboard/home tried to use $ before it was available
   - Error: "Uncaught ReferenceError: $ is not defined"

2. **Turbolinks conflict**
   - Changed data-turbo-track but app still uses turbolinks gem
   - Error: "Cannot set properties of undefined (setting 'Turbolinks')"
   - Both turbolinks and turbo-rails in Gemfile causing conflicts

3. **type="module" breaking globals**
   - ES6 modules have their own scope
   - Prevented jQuery from being global window.$
   - Broke all existing jQuery-dependent code

## Solutions Applied

1. **Script Load Order**
   ```html
   <!-- BEFORE: Wrong order -->
   <%= javascript_include_tag "application" %>
   <script src="jquery.min.js"></script>

   <!-- AFTER: Correct order -->
   <script src="jquery.min.js"></script>
   <%= javascript_include_tag "application" %>
   <script src="bootstrap.bundle.min.js"></script>
   ```

2. **Reverted to Turbolinks**
   ```erb
   <!-- Changed back from: -->
   "data-turbo-track": "reload"

   <!-- To original: -->
   "data-turbolinks-track" => "reload"
   ```

3. **Removed type="module"**
   ```html
   <!-- Before: -->
   <%= javascript_include_tag "application", type: "module" %>

   <!-- After: -->
   <%= javascript_include_tag "application" %>
   ```

## Technical Details

**Script execution order:**
1. jQuery (CDN) - Makes $ available globally
2. Bootstrap CSS (CDN) - Styles load early
3. application.css (Rails) - Custom styles
4. application.js (Rails) - Can now use jQuery
5. Bootstrap JS (CDN) - Needs jQuery, loaded last

**Why this order matters:**
- application.js likely has jQuery dependencies
- Dashboard charts/graphs use jQuery
- Bootstrap 5 JS doesn't require jQuery but loads after for safety
- Turbolinks needs to initialize before page interactions

**Compatibility:**
- Keeps existing jQuery-dependent code working
- Maintains Turbolinks behavior (app has both gems)
- All dashboard statistics/charts now load correctly
- No breaking changes to existing pages

This maintains backward compatibility while preserving the modern UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed "Cannot read properties of undefined (reading 'update')" errors
caused by chart setTimeout callbacks persisting across Turbolinks page
navigations.

Changes:
- Add existence checks before initializing charts
- Guard all .update() calls with element and instance checks
- Track all setTimeout IDs in chartTimeouts array
- Clear timeouts on Turbolinks navigation events
- Clear timeouts at start of pieChartHome() to prevent duplicates

This ensures chart update callbacks only run when chart elements exist
on the page, preventing errors when navigating to pages without charts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed "Cannot read properties of undefined (reading 'arrayToDataTable')"
error caused by calling Google Charts API before it finished loading.

Changes:
- Move google.load() call below function definitions
- Use callback parameter to ensure charts load after library is ready
- Add guard check in drawChart2() to verify google.visualization exists
- Wrap chart drawing in $(document).ready() within the callback

This ensures the visualization library is fully loaded before attempting
to create charts, preventing race condition errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed $(document).ready() wrapper inside google.load callback which
was preventing charts from rendering when page loaded via Turbolinks.

Changes:
- Remove document.ready wrapper (DOM already ready with Turbolinks)
- Add check for element existence before drawing chart
- Add guard to verify google.load exists before calling
- Create separate initializeChart function for cleaner callback

This ensures charts render properly on Turbolinks page loads where
the DOM is already ready when the script executes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Added comprehensive Turbolinks event handling and duplicate load
prevention for Google Charts on performance page.

Changes:
- Add turbolinks:load event listener for page navigations
- Prevent multiple google.load() calls with flag
- Check if visualization already loaded before loading again
- Add chart element existence check before drawing
- Call initializeChart() immediately for initial load
- Better error messages for debugging

This ensures charts render on both initial page load and Turbolinks
navigation, while preventing duplicate library loads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed "undefined method stringify_keys for String" error caused by
incorrect button_to syntax when using block form.

Changes:
- Remove text argument from button_to when using block
- Block content becomes button text in Rails 8 syntax
- Correct syntax: button_to url, options do ... end
- Incorrect syntax: button_to "text", url, options do ... end

This fixes the NoMethodError on the login page.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed "stringify_keys for String" errors on Sign Up and Login buttons
by removing text arguments from button_to when using block form.

Changes:
- Fix Sign Up button: button_to signup_path (not "Sign Up", signup_path)
- Fix Login button: button_to login_path (not "Login", login_path)
- Block content now provides button text in Rails 8

All button_to calls now use correct Rails 8 syntax.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed Bootstrap Icon being cut off in navbar by adding proper flexbox
alignment and line-height controls to the brand link.

Changes:
- Add display: inline-flex to .rg-brand for proper icon alignment
- Add align-items: center to vertically center icon with text
- Add gap: 0.5rem for spacing between icon and text
- Set line-height: 1 to prevent extra vertical space
- Make icon slightly larger (1.75rem) for better visual hierarchy

This ensures the shield icon displays fully without being clipped.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed shield icon being cut off by adding container padding and
ensuring proper spacing from viewport edge.

Changes:
- Add overflow: visible to .rg-header to prevent clipping
- Increase container-fluid padding to 2rem for edge spacing
- Remove left padding from first col-auto to align with container
- Add min-width to icon for consistent sizing
- Remove negative row margins that could cause cutoff

The icon now has proper space from the viewport edge and displays
fully without being clipped down the middle.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed modal not displaying by replacing button_to with regular button
element and adding proper Turbolinks event handling.

Changes:
- Replace button_to with <button> element for proper ID targeting
- Add Turbolinks event listener (turbolinks:load) for navigation
- Clone button to remove duplicate event listeners
- Add error handling for fetch failures
- Remove Bootstrap data attributes (using JS instead)

The button_to helper creates a form which interfered with the
JavaScript event listener and Bootstrap modal initialization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed static aria-hidden attribute from modal element to fix
"Blocked aria-hidden on an element because its descendant retained
focus" accessibility warning.

Changes:
- Remove aria-hidden="true" from modal root element
- Add role="document" to modal-dialog for better accessibility
- Let Bootstrap 5 manage aria-hidden dynamically on open/close

The static aria-hidden="true" was conflicting with focus management
when the modal opened. Bootstrap 5 handles this attribute dynamically,
so it should not be set in the HTML.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Added event listeners to manage aria-hidden attribute timing during
modal open/close transitions to prevent accessibility warnings.

Changes:
- Listen to hide.bs.modal to remove aria-hidden before closing
- Listen to hidden.bs.modal to restore aria-hidden after fully closed
- Listen to show.bs.modal to remove aria-hidden when opening
- Use setTimeout to ensure focus has moved before setting aria-hidden

This prevents the "Blocked aria-hidden on element with focus" warning
by ensuring aria-hidden is only set after focus has left the modal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Added console logging to diagnose why Demo Credentials modal
is not opening despite no visible errors.

Changes:
- Log button click event
- Log Bootstrap availability check
- Log modal element existence
- Log fetch response status
- Log content length after loading
- Log modal instance creation
- Check Bootstrap.Modal availability before use

This will help identify whether the issue is with event binding,
Bootstrap loading, fetch requests, or modal initialization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed debugging code and aria-hidden event listeners that were
preventing the modal from displaying. Using Bootstrap's
getOrCreateInstance() to avoid modal instance conflicts.

Changes:
- Remove aria-hidden event listeners that blocked modal display
- Remove debugging console.log statements
- Use Modal.getOrCreateInstance() instead of new Modal()
- Simplify event handler to essential functionality only

The aria-hidden event listeners were preventing the modal from
showing properly. getOrCreateInstance() prevents duplicate modal
instances that can cause display issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed modal showing backdrop but not the modal itself by explicitly
disposing old instances and adding a timing delay.

Changes:
- Dispose of existing modal instance before creating new one
- Create fresh modal with explicit options (backdrop, keyboard, focus)
- Add 10ms setTimeout before show() to ensure DOM readiness
- Remove getOrCreateInstance which was causing conflicts

The modal was creating a backdrop but staying display:none because
getOrCreateInstance was returning a stale modal instance that couldn't
properly transition. Disposing and recreating fixes this.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed complex modal implementation and replaced with simple
link to dedicated credentials page to eliminate all modal issues.

Changes:
- Add credentials action to TutorialsController
- Remove layout false restriction for credentials
- Replace button with simple link_to for Demo Credentials
- Remove entire modal HTML structure
- Remove all JavaScript for modal initialization
- Remove fetch/AJAX complexity

The credentials view already existed but was modal-only. Now it's
a proper page that users can navigate to directly. Much simpler!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Apply modern design system principles to replace dated 2013-era styling:

Buttons:
- Rounded corners (0.75rem border-radius)
- Gradient backgrounds with depth
- Smooth hover animations (translateY + shadow)
- Soft box shadows (0 1px 3px → 0 4px 12px on hover)

Cards & Widgets:
- Increased border-radius (1rem)
- Softer shadows (0 2px 8px rgba)
- Hover effects with elevated shadows
- Clean header separation without borders

Forms:
- Rounded inputs (0.75rem)
- Thicker borders (2px) for clarity
- Focus rings with brand color
- Better padding for touch targets

Header:
- Backdrop blur effect (frosted glass)
- Semi-transparent background (rgba 0.95)
- Removed hard borders for cleaner look
- Larger, softer shadows

Tables & Dropdowns:
- Rounded tables with overflow hidden
- Subtle row hover effects
- Modern dropdown styling with shadows
- Smooth transitions on all interactions

This addresses the feedback that buttons were "blocky/chunky and still
resemble websites from 2013" by implementing 2024 design trends.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Changed the logo from a non-interactive <span> to a clickable <a> link
pointing to the login page for unauthenticated users. This provides a
consistent navigation pattern across authenticated and unauthenticated
states.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removed leftover modal HTML fragments (modal-header, modal-footer,
data-dismiss="modal") that were causing accessibility errors and
non-functional close button.

Replaced with clean, standalone card-based layout with:
- Proper close button linking to homepage
- Bootstrap card structure with modern styling
- Working "Show Credentials" button with jQuery
- "Back to Home" link in footer
- Removed problematic aria-hidden attributes

Fixes: "Blocked aria-hidden on an element because its descendant
retained focus" accessibility error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Transformed key user-facing pages with modern UI patterns:

**Login Page (sessions/new.html.erb)**:
- Enhanced warning box with gradient background and backdrop blur
- Added arrow indicator to "Learn more" link
- Improved visual hierarchy with better icon sizing

**Signup Page (users/new.html.erb)**:
- Complete rewrite from Bootstrap 2 to Bootstrap 5
- Modern card-based layout matching login page aesthetic
- Icon-enhanced form inputs with proper labels
- Side-by-side first/last name fields
- Gradient info box with training environment notice
- Proper form validation attributes

**Dashboard Home (dashboard/home.html.erb)**:
- Replaced old .span12/.row-fluid with modern grid
- Clean card-based layout with shadow
- Icon-enhanced header and buttons
- Loading spinner states during chart transitions
- Active button state indicators for chart type toggle
- Turbolinks compatibility
- Improved accessibility with ARIA labels

All pages now feature:
- Bootstrap 5 components and utilities
- Bootstrap Icons integration
- Rounded corners and modern spacing
- Gradient accents and visual depth
- Smooth transitions and hover states

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
**Password Reset Pages**:

Forgot Password (password_resets/forgot_password.html.erb):
- Complete rewrite with modern card-based layout
- Icon-enhanced form with email validation
- Helpful info box with reset instructions
- "Back to Login" link for easy navigation
- Gradient background matching login page style

Reset Password (password_resets/reset_password.html.erb):
- Modern shield-lock icon header
- Password strength guidance with form text
- Confirmation field with proper validation
- Security tips info box with gradient styling
- Consistent with overall auth page design

**Admin Dashboard (admin/dashboard.html.erb)**:
- Replaced Bootstrap 2 classes with Bootstrap 5
- Modern alert design with icons and close buttons
- Card-based layout with subtle shadow
- Loading spinner state for user table
- Icon-enhanced header (people icon)
- Turbolinks compatibility
- Improved accessibility with ARIA labels

All pages now feature:
- Bootstrap 5 modern components
- Bootstrap Icons integration
- Rounded corners and gradient accents
- Smooth transitions and hover states
- Proper loading states and feedback
- Consistent design language across the app

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
The bar graph was calling drawChart3() before Google Charts library
finished loading, causing "Cannot read properties of undefined
(reading 'arrayToDataTable')" error.

Applied same fix as performance page:
- Check if visualization already loaded before calling google.load
- Use callback parameter to ensure charts only draw after load
- Add flag to prevent duplicate library loads
- Guard against missing DOM elements
- Handle AJAX-loaded partial context

Fixes dashboard statistics bar graph view errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
The issue was that google.load() doesn't work reliably when called
from AJAX-loaded content. The callback wasn't firing.

Solution:
- Load Google Charts library once in main application.html.erb layout
- Bar graph partial now just polls for google.visualization to be ready
- Uses retry logic (50 attempts @ 100ms = 5 second timeout)
- Returns success/failure boolean for proper flow control
- Removed duplicate script loading from partial

This ensures Google Charts is available globally for all chart views
(bar graphs, pie charts, performance charts) without timing issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
The deprecated Google JSAPI (google.load) was failing to load reliably,
causing the bar graph view to timeout after 5 seconds. Google Charts
with the old jsapi has been deprecated and has timing/loading issues,
especially with AJAX and Turbolinks.

Solution:
- Replaced bar chart with clean, modern table showing same data
- Added colorful stat summary cards with totals
- Removed unreliable Google Charts library from layout
- No JavaScript dependencies or loading delays
- Instant rendering, works perfectly with AJAX loading

The new view:
- Clean responsive table with hover effects
- 4 summary cards showing total visitors, orders, income, expenses
- Color-coded borders matching original chart colors
- Modern card design consistent with rest of the app
- Works immediately without any loading or timing issues

Note: Pie charts and performance charts still use their own
Google Charts loading, which works in their specific context.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Complete redesign of the PTO management page:

**Layout Improvements**:
- Migrated from Bootstrap 2 to Bootstrap 5 grid system
- Replaced .span classes with modern .col classes
- Side-by-side calendar and form layout on desktop
- Responsive cards with proper spacing

**Removed Google Charts**:
- Replaced sick days chart with 3 colorful stat cards
- Replaced PTO chart with 3 colorful stat cards
- Shows Earned, Taken, Remaining at a glance
- Color-coded with left borders (blue, red, green)
- No loading delays or JavaScript errors

**Modern Form**:
- Bootstrap 5 form controls with proper labels
- Icon-enhanced input groups
- Rounded inputs with better spacing
- Primary button for submission
- Form clears after successful submission

**Enhanced Calendar**:
- Kept FullCalendar but styled with modern theme
- Rounded corners and better button styling
- Brand-colored buttons and events
- Responsive layout

**Improved Alerts**:
- Bootstrap 5 dismissible alerts
- Icon-enhanced success/error messages
- Better visual hierarchy

**Additional Polish**:
- Formatted dates ("December 07, 2024" format)
- Info icons with contextual help
- Card shadows for depth
- Consistent spacing throughout
- Turbolinks compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
cktricky and others added 23 commits December 7, 2025 02:41
Complete redesign of the benefit forms download and upload page:

**Download Section**:
- Beautiful hover cards for Health and Dental insurance
- Large colorful icons (heart pulse and smile)
- Card elevation on hover (lift animation)
- Primary colored Health button, success colored Dental button
- Centered layout with descriptions
- Side-by-side responsive layout

**Upload Section**:
- Modern drag-drop style upload area
- Dashed border with cloud upload icon
- Custom file input with "Choose File" button
- Real-time file selection feedback
- Upload area changes color when file selected (green border)
- Animated progress bar during upload
- Cancel button to reset form
- Clean action buttons with icons

**Additional Features**:
- Info box with important upload guidelines
- File format and size restrictions
- Bootstrap Icons throughout
- Smooth transitions and animations
- Turbolinks compatibility
- Form validation (file required)
- Simulated upload progress visualization

**Removed**:
- Old Bootstrap 2 classes (span4, span12)
- Outdated icon fonts
- Complex file upload plugin dependencies
- Cluttered table-heavy layout

The page now looks like a modern web application with:
- Card-based design
- Hover effects
- Large touch-friendly buttons
- Clear visual hierarchy
- Professional polish

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Transforms the retirement benefits page with a modern, engaging design:

Design improvements:
- Add piggy bank icon header with descriptive subtitle
- Create three large contribution stat cards with hover effects:
  * Employee Contribution (blue with person-check icon)
  * Employer Contribution (green with building-check icon)
  * Total Contribution (red gradient with cash-stack icon)
- Stat cards lift on hover with shadow deepening and number scaling
- Add featured Employee Services card with 4rem icon and gradient highlight
- Include three smaller info cards for Investment Options, Employer Matching, Tax Advantages
- Apply colored left/top borders, rounded corners, and smooth animations
- Ensure Turbolinks compatibility with proper event handling

The page now provides a visually appealing, easy-to-scan view of retirement
benefits that matches the modern design system.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Resolves "Uncaught TypeError: $(...).fullCalendar is not a function"
by loading FullCalendar and Moment.js libraries from CDN.

Changes:
- Add Moment.js 2.29.4 from CDN to application layout
- Add FullCalendar 3.10.5 CSS and JS from CDN
- Remove local javascript_include_tag calls from PTO page
- Ensure libraries load before page attempts to initialize calendar

The PTO calendar now loads reliably across page navigations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Improves the Schedule PTO form section with modern design elements:

Design enhancements:
- Add left border accent in primary color to highlight the card
- Add gradient background to header with descriptive subtitle
- Include icons next to each form label (tag, chat, calendar)
- Upgrade all form controls to large size for better touch targets
- Add helpful placeholder text with examples (e.g., "Summer Vacation")
- Include descriptive helper text below fields for guidance
- Make submit button full-width and large for prominence
- Add tip box at bottom with success border highlighting post-submission info
- Increase padding and spacing (mb-4) for better breathing room

The form now feels more guided, professional, and easier to use.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Complete redesign of the performance page with modern Bootstrap 5:

Major improvements:
- Add header with graph icon and descriptive subtitle
- Create four stat cards showing key metrics at a glance:
  * Average Score (blue with star icon)
  * Highest Score (red with trophy icon)
  * Latest Score (green with calendar icon)
  * Total Reviews (purple with document icon)
- Stat cards lift and scale numbers on hover
- Modernize chart card with better spacing and min-height
- Enhance chart styling with smooth curves and better colors
- Transform table with modern header styling and icons
- Add reviewer avatars (circular icons) in table rows
- Color-code scores with badges (green=5, blue=4, yellow=3, red<3)
- Add empty state with inbox icon for no reviews
- Replace old Bootstrap 2 classes (row-fluid, span12, widget)
- Use Bootstrap 5 grid system and modern card components
- Add hover effects on table rows and stat cards

The page now provides an engaging, data-rich view of performance history
with clear visual hierarchy and modern design patterns.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removes problematic Google Charts dependency and creates a cleaner,
more reliable performance trend visualization.

Changes:
- Remove all Google Charts JavaScript code (100+ lines)
- Replace chart with visual timeline showing each review chronologically
- Each timeline item displays:
  * Date at top, reviewer name at bottom
  * Colored circular badge with score number (green=5, blue=4, yellow=3, red<3)
  * Horizontal progress bar showing score percentage with comments
- Add smooth animations: fade-in on load, scale on dot hover, slide on bar hover
- Color-coded by score for instant visual feedback
- Fully responsive with mobile layout
- No external dependencies - pure CSS solution
- Add empty state with graph icon if no performance data

The timeline provides better visual hierarchy and eliminates the blank
space issue caused by Google Charts loading failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Complete redesign of the messaging interface with modern layout:

Inbox improvements:
- Replace table with modern message cards
- Each message shows circular gradient avatar with person icon
- Display sender name prominently with formatted date
- Show full message text with proper line wrapping
- Add Details and Delete action buttons with icons
- Hover effect highlights each message
- Beautiful empty state with inbox icon when no messages

Send Message form:
- Relocate to right sidebar with sticky positioning
- Add green gradient header with send icon
- Style as modern card with left border accent
- Large form controls with icons for better UX
- Recipient selector with all users
- Expandable textarea for message composition
- Full-width send button in success green
- Helpful tip box below form
- Modern Bootstrap 5 alerts with icons for success/error
- Auto-reload page after successful send to show new message

Layout enhancements:
- Two-column responsive layout (8/4 split)
- Inbox on left, compose on right
- Sticky compose form stays visible while scrolling
- Mobile-friendly with stacked layout on small screens
- Replace all Bootstrap 2 classes (row-fluid, span12, widget)
- Modern Bootstrap 5 grid and components
- Turbolinks compatibility

The page now provides a clean, modern messaging experience similar
to contemporary email/messaging applications.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Complete redesign of the pay/direct deposit management page:

Layout improvements:
- Two-column responsive layout (forms left, table right)
- Forms column (4/12):
  * Add Direct Deposit form with green theme and gradient header
  * Decrypt Account form with yellow/warning theme
  * Both cards have left border accents
- Table column (8/12):
  * DataTable showing existing accounts
  * "Why Encrypted?" button in header
  * Three info cards below explaining benefits

Form enhancements:
- All form controls upgraded to large size with icons
- Input groups with trailing icons (bank, routing, lock, percent)
- Helper text below each field for guidance
- Full-width submit buttons in themed colors
- Tip boxes with security/convenience info
- Auto-clear forms after successful submission

Table improvements:
- Modern Bootstrap 5 table with hover effects
- Icons in column headers (lock, diagram, percent, gear)
- Enhanced data display:
  * Account numbers in monospace code blocks
  * Routing numbers in light badges
  * Deposit percentages in green success badges
  * Delete buttons styled as outline-danger with trash icon
- Custom DataTables pagination styling matching theme
- Empty state message for no accounts

JavaScript enhancements:
- Replace basic alerts with modern Bootstrap-styled overlays
- Decrypted account number shows in floating alert with unlock icon
- "Why Encrypted?" shows modal-like dialog with close button
- Delete confirmation improved
- Turbolinks compatibility
- Form reset after success

Info cards:
- Instant Access (blue) - explain direct deposit timing
- Secure & Encrypted (green) - highlight security features
- Split Deposits (yellow) - describe multi-account feature

The page now provides a banking-grade interface for managing
direct deposit with clear visual hierarchy and modern UX.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Resolves "Cannot set properties of undefined (setting '_DT_CellIndex')"
error by modernizing DataTables API usage and handling Turbolinks properly.

Changes:
- Update to modern DataTables API (capital D DataTable() vs lowercase)
- Add check for existing DataTable before initialization
- Properly destroy and recreate DataTable on Turbolinks page loads
- Replace deprecated fnClearTable() with table.clear()
- Replace deprecated fnAddData() with table.row.add() + table.draw()
- Create unified initializePage() function for both ready and turbolinks:load
- Add autoWidth, searching, and ordering options to DataTable config

The DataTable now initializes cleanly without errors and handles
Turbolinks navigation properly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Transforms blocky form controls into sleek, modern inputs:

Form control improvements:
- Rounded corners (0.75rem) for all inputs and buttons
- 2px borders with light gray color (#e9ecef)
- Subtle shadows for depth (0 1px 3px rgba)
- Smooth transitions on all interactions (0.2s ease)
- Larger padding for better touch targets

Focus state enhancements:
- Add Direct Deposit form: green glow on focus with 4px shadow ring
- Decrypt form: yellow/warning glow on focus with themed shadow
- Input group icons change gradient on focus
- Entire input group highlights together (border color sync)
- Remove harsh outline, replace with soft shadow

Button refinements:
- More rounded corners (0.75rem)
- Lift effect on hover (translateY -2px)
- Enhanced shadows that grow on hover
- Smooth press animation on active state
- Bold font weight (600)

Input group styling:
- Gradient backgrounds on addon icons
- Seamless connection between input and icon
- Icons highlight with themed gradient on focus
- Smooth border radius flow from input to addon

The forms now have a polished, modern appearance matching
contemporary web applications with smooth, delightful interactions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Resolves icon height mismatch with form inputs.

Changes:
- Add explicit padding to input-group-text matching form-control
- Use flexbox (display: flex, align-items: center) for vertical centering
- Match padding for input-group-lg contexts (0.875rem 1.25rem)
- Set icon font-size to 1rem and line-height: 1 to prevent overflow
- Add min-width: 50px for consistent icon container size

Icons now align perfectly with input heights for a polished appearance.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removes visual clutter and simplifies the interface for better usability:

Layout simplification:
- Change column split from 4/8 to 5/7 for better balance
- Remove gradient backgrounds from card headers
- Use simple white headers with clean icons
- Increase spacing between sections (g-4 gap)
- Remove info cards at bottom to reduce page length

Form simplification:
- Remove input group icons and addons
- Use clean standalone inputs without decorations
- Remove helper text under each field (info in placeholder)
- Reduce button sizes from btn-lg to standard
- Remove decorative tip boxes
- Simpler labels without icons
- Reduce vertical spacing (mb-3 instead of mb-4)

Table simplification:
- Remove icons from table headers
- Cleaner header text ("Your Accounts" vs "Direct Deposit Accounts")
- Remove subtitle text from headers

Input styling:
- Smaller, cleaner inputs (0.5rem radius, 1px border)
- Smaller padding (0.625rem vs 0.875rem)
- Smaller font size (0.95rem)
- Subtle focus rings (3px glow)
- Color-coded focus: green for add, yellow for decrypt
- Removed complex gradients and shadows

The page now has a clean, uncluttered appearance with better
visual hierarchy and easier-to-scan content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Makes form controls more rounded and polished instead of rectangular:

Form control updates:
- Increase border-radius from 0.5rem to 0.75rem for softer curves
- Increase border from 1px to 2px for better definition
- Adjust padding to 0.75rem 1rem for comfortable spacing
- Set font-size to 1rem for better readability

Button updates:
- Match border-radius at 0.75rem for consistency
- Increase font-weight to 600 for emphasis
- Add explicit padding (0.75rem 1.5rem)
- Stronger hover lift effect (translateY -2px)
- Enhanced shadow on hover (0 4px 12px)

Inputs and buttons now have modern, rounded appearance matching
the design system used throughout the application.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Updates pay page forms to use the same styling patterns as messages,
PTO, and other modernized pages for consistency.

Changes:
- Remove local form control styles that override global styles
- Use global form styles from application.html.erb
- Add form-control-lg class to all inputs for larger size
- Add icons to all form labels (bank2, diagram-3, percent, key-fill)
- Use fw-semibold class on labels for bold appearance
- Add helper text below inputs with small.text-muted
- Change spacing from mb-3 to mb-4 for consistency
- Use btn-lg class for all buttons
- Keep only custom focus colors (green for add, yellow for decrypt)

Forms now match the polished appearance of other pages with:
- Properly rounded inputs (0.75rem from global styles)
- 2px borders with nice focus effects
- Larger, more comfortable controls
- Helpful icons and descriptions
- Consistent spacing and typography

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Replace user data in seeds:
- Jack Mannino → John Smith ([email protected])
- Jim Manico → James Anderson ([email protected])

Update wiki documentation examples to use new names.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fix "Illegal invocation" JavaScript error when opening edit modal:
- Remove Bootstrap 2 'hide' class from modal markup
- Add proper Bootstrap 5 modal structure (modal-dialog/modal-content)
- Update JavaScript to use Bootstrap 5 Modal API
- Load dynamic content into .modal-content instead of root modal
- Remove legacy data-toggle attribute from button

The modal now uses the correct Bootstrap 5.3 structure and API,
resolving selector-engine.js errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Update modal content to Bootstrap 5 styling and API:
- Replace Bootstrap 2 modal-header structure with Bootstrap 5
- Update close button from 'close' class to 'btn-close'
- Replace 'data-dismiss' with 'data-bs-dismiss'
- Modernize form classes: control-group → mb-3, span12 → form-control
- Update form labels to use 'form-label' class
- Add 'form-select' class to select dropdown
- Update JavaScript to use Bootstrap 5 Modal.getInstance() API
- Add preventDefault() to button click handlers

The modal now properly loads and displays in Bootstrap 5 with
modern form styling and correct modal dismissal behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add 'return false;' to onClick handler to prevent the # href
from causing page navigation/redirect to dashboard.

This fixes the issue where clicking Edit would redirect to
/admin/1/dashboard# instead of opening the modal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add console logging to openEditModal function to debug AJAX load
- Add explicit id and name attributes to admin select field
- Only show modal after content successfully loads
- Log errors if modal content fails to load

This helps diagnose the modal loading issue and fixes the Chrome
warning about form fields lacking id/name attributes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Log modal element to verify it exists
- Check for existing modal instance before creating new one
- Log each step of modal creation and show process

This helps identify why modal.show() isn't displaying the modal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Switch from bootstrap.Modal class to jQuery .modal('show') method.
Bootstrap 5 still supports the jQuery plugin API for backwards
compatibility, and this method handles initialization automatically.

This should fix the issue where modal.show() was called but the
modal wasn't appearing visually.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Bootstrap 5 removed jQuery plugin support, so .modal('show') doesn't work.
Switch back to native Bootstrap 5 Modal API with proper initialization:

- Dispose of any existing modal instance before creating new one
- Create modal with explicit options (backdrop, keyboard, focus)
- Add detailed console logging for each step

This ensures the modal is properly initialized before showing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove complex modal implementation and replace with simple page navigation:
- Convert get_user view from modal partial to full edit page
- Add proper form with Bootstrap 5 styling
- Link directly from users list to edit page
- Update controller actions to redirect instead of returning JSON
- Add flash messages for success/error feedback
- Remove all modal JavaScript and markup
- Remove modal CSS and backdrop handling

Benefits:
- Much simpler and more maintainable
- No JavaScript errors or complexity
- Standard Rails CRUD pattern
- Better user experience with proper navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@cktricky cktricky merged commit b6270db into master Dec 7, 2025
6 of 7 checks passed
@cktricky cktricky deleted the rails-8-upgrade branch December 7, 2025 22:38
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