Initial commit (history cleared)
CI / test (3.4.1) (push) Has been cancelled

This commit is contained in:
2026-04-29 11:21:39 +01:00
commit 298610b5f6
277 changed files with 30877 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
# frozen_string_literal: true
class AdminController < ApplicationController
before_action :administrative, if: :admin_param, except: [:get_user]
skip_before_action :has_info
def dashboard
end
def analytics
if params[:field].nil?
fields = "*"
else
fields = custom_fields.join(",")
end
if params[:ip]
@analytics = Analytics.hits_by_ip(params[:ip], fields)
else
@analytics = Analytics.all
end
end
def get_all_users
@users = User.all
end
def get_user
@user = User.find_by_id(params[:admin_id].to_s)
arr = ["true", "false"]
@admin_select = @user.admin ? arr : arr.reverse
end
def update_user
user = User.find_by_id(params[:admin_id])
if user
# VULNERABILITY: Using params[:user] directly without strong parameters
# This allows mass assignment of any user attribute including 'admin'
# See wiki: Extras:-Mass-Assignment-Admin-Role.md
user_params = params[:user].to_unsafe_h if params[:user].respond_to?(:to_unsafe_h)
user_params ||= params[:user]
# Filter out password fields if blank to avoid validation errors
filtered_params = user_params.reject { |k, v| (k == "password" || k == "password_confirmation") && v.blank? }
user.update(filtered_params)
user.save!
flash[:success] = "User updated successfully"
redirect_to admin_get_all_users_path(current_user.id)
else
flash[:error] = "User not found"
redirect_to admin_get_all_users_path(current_user.id)
end
end
def delete_user
user = User.find_by(id: params[:admin_id])
if user && !(current_user.id == user.id)
# Call destroy here so that all association records w/ id are destroyed as well
# Example user.retirement records would be destroyed
user.destroy
flash[:success] = "User deleted successfully"
else
flash[:error] = "Cannot delete this user"
end
redirect_to admin_get_all_users_path(current_user.id)
end
private
def custom_fields
params.require(:field).keys
end
helper_method :custom_fields
def admin_param
params[:admin_id] != "1"
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class Api::V1::MobileController < ApplicationController
skip_before_action :authenticated
before_action :mobile_request?
respond_to :json
def show
if params[:class]
model = params[:class].classify.constantize
respond_with model.find(params[:id]).to_json
end
end
def index
if params[:class]
model = params[:class].classify.constantize
respond_with model.all.to_json
else
respond_with nil.to_json
end
end
private
def mobile_request?
if session[:mobile_param]
session[:mobile_param] == "1"
else
request.user_agent =~ /ios|android/i
end
end
end
@@ -0,0 +1,56 @@
# frozen_string_literal: true
class Api::V1::UsersController < ApplicationController
skip_before_action :authenticated
before_action :valid_api_token
before_action :extrapolate_user
respond_to :json
def index
respond_with @user.admin ? User.all : @user
end
def show
respond_with @user.as_json
end
private
def valid_api_token
authenticate_or_request_with_http_token do |token, options|
# TODO :add some functionality to check if the HTTP Header is valid
if !identify_user(token)
redirect_to root_url
else
true
end
end
end
def identify_user(token = "")
# We've had issues with URL encoding, etc. causing issues so just to be safe
# we will go ahead and unescape the user's token
unescape_token(token)
@clean_token =~ /(.*?)-(.*)/
id = $1
hash = $2
check_hash(id, hash)
end
def check_hash(id, hash)
digest = OpenSSL::Digest::SHA1.hexdigest("#{ACCESS_TOKEN_SALT}:#{id}")
hash == digest
end
# We had some issues with the token and url encoding...
# this is an attempt to normalize the data.
def unescape_token(token = "")
@clean_token = CGI::unescape(token)
end
# Added a method to make it easy to figure out who the user is.
def extrapolate_user
@user = User.find_by_id(@clean_token.split("-").first)
end
end
+61
View File
@@ -0,0 +1,61 @@
# frozen_string_literal: true
class ApplicationController < ActionController::Base
before_action :authenticated, :has_info, :create_analytic, :mailer_options
helper_method :current_user, :is_admin?, :sanitize_font
# Our security guy keep talking about sea-surfing, cool story bro.
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
#protect_from_forgery with: :exception
private
def mailer_options
ActionMailer::Base.default_url_options[:protocol] = request.protocol
ActionMailer::Base.default_url_options[:host] = request.host_with_port
end
def current_user
@current_user ||= (
User.find_by(auth_token: cookies[:auth_token].to_s) ||
User.find_by(id: session[:user_id].to_s)
)
end
def authenticated
path = request.fullpath.present? ? root_url(url: request.fullpath) : root_url
redirect_to path and reset_session if !current_user
end
def is_admin?
current_user.admin if current_user
end
def administrative
if !is_admin?
redirect_to root_url
end
end
def has_info
redirect = false
if current_user
begin
if !(current_user.retirement || current_user.paid_time_off || current_user.paid_time_off.schedule || current_user.work_info || current_user.performance)
redirect = true
end
rescue
redirect = true
end
end
redirect_to home_dashboard_index_path if redirect
end
def create_analytic
Analytics.create({ ip_address: request.remote_ip, referrer: request.referrer, user_agent: request.user_agent})
end
def sanitize_font(css)
css
end
end
@@ -0,0 +1,80 @@
# frozen_string_literal: true
class BenefitFormsController < ApplicationController
def index
@benefits = Benefits.new
load_uploaded_files
end
def download
begin
path = params[:name]
file = params[:type].constantize.new(path)
send_file file, disposition: "attachment"
rescue
redirect_to user_benefit_forms_path(user_id: current_user.id)
end
end
def redirect_to_benefit_forms
flash[:info] = "Please use the upload form below to upload files"
redirect_to user_benefit_forms_path(user_id: current_user.id)
end
def upload
file = params[:benefits][:upload]
if file.nil?
flash.now[:error] = "Please select a file to upload"
@benefits = Benefits.new
load_uploaded_files
render :index
return
end
# Validate file type
allowed_extensions = %w[.pdf .doc .docx .jpg .jpeg .png]
file_extension = File.extname(file.original_filename).downcase
unless allowed_extensions.include?(file_extension)
flash.now[:error] = "Invalid file type. Accepted formats: PDF, DOC, DOCX, JPG, PNG. You uploaded: #{file_extension}"
@benefits = Benefits.new
load_uploaded_files
render :index
return
end
# Validate file size (10MB max)
max_size = 10.megabytes
if file.size > max_size
flash.now[:error] = "File too large. Maximum size: 10MB. Your file: #{(file.size / 1.megabyte.to_f).round(2)}MB"
@benefits = Benefits.new
load_uploaded_files
render :index
return
end
begin
Benefits.save(file, params[:benefits][:backup])
flash[:success] = "File '#{file.original_filename}' uploaded successfully!"
rescue => e
flash[:error] = "Failed to upload file: #{e.message}"
end
redirect_to user_benefit_forms_path(user_id: current_user.id)
end
private
def load_uploaded_files
data_path = Rails.root.join("public", "data")
@uploaded_files = Dir.glob("#{data_path}/*").reject { |f| File.directory?(f) || File.basename(f) == '.gitkeep' }.map do |file|
{
name: File.basename(file),
size: File.size(file),
modified: File.mtime(file)
}
end.sort_by { |f| f[:modified] }.reverse
end
end
View File
+25
View File
@@ -0,0 +1,25 @@
# frozen_string_literal: true
class DashboardController < ApplicationController
skip_before_action :has_info
layout false, only: [:change_graph]
def home
@user = current_user
# See if the user has a font preference
if params[:font]
cookies[:font] = params[:font]
end
end
def change_graph
self.try(params[:graph])
if params[:graph] == "bar_graph"
render "dashboard/bar_graph"
else
@user = current_user
render "dashboard/pie_charts"
end
end
end
+44
View File
@@ -0,0 +1,44 @@
# frozen_string_literal: true
class MessagesController < ApplicationController
def index
@messages = current_user.messages
@message = Message.new
sleep(3)
end
def show
@message = Message.where(id: params[:id]).first
end
def destroy
message = Message.where(id: params[:id]).first
if message.destroy
flash[:success] = "Your message has been deleted."
redirect_to user_messages_path(user_id: current_user.id)
else
flash[:error] = "Could not delete message."
end
end
def create
if Message.create(message_params)
respond_to do |format|
format.html { redirect_to user_messages_path(user_id: current_user.id) }
format.json { render json: {msg: "success"} }
end
else
respond_to do |format|
format.html { redirect_to user_messages_path }
format.json { render json: {msg: "failure"} }
end
end
end
private
def message_params
params.require(:message).permit(:creator_id, :message, :read, :receiver_id)
end
end
@@ -0,0 +1,8 @@
# frozen_string_literal: true
class PaidTimeOffController < ApplicationController
def index
@pto = current_user.paid_time_off
@schedule = Schedule.new
end
end
@@ -0,0 +1,63 @@
# frozen_string_literal: true
class PasswordResetsController < ApplicationController
skip_before_action :authenticated
def reset_password
user = Marshal.load(Base64.decode64(params[:user])) unless params[:user].nil?
if user && params[:password] && params[:confirm_password] && params[:password] == params[:confirm_password]
user.password = params[:password]
user.save!
flash[:success] = "Your password has been reset please login"
redirect_to :login
else
flash[:error] = "Error resetting your password. Please try again."
redirect_to :login
end
end
def confirm_token
if !params[:token].nil? && is_valid?(params[:token])
flash[:success] = "Password reset token confirmed! Please create a new password."
render "password_resets/reset_password"
else
flash[:error] = "Invalid password reset token. Please try again."
redirect_to :login
end
end
def send_forgot_password
@user = User.find_by_email(params[:email]) unless params[:email].nil?
if @user && password_reset_mailer(@user)
flash[:success] = "Password reset email sent to #{params[:email]}"
redirect_to :login
else
flash[:error] = "There was an issue sending password reset email to #{params[:email]}".html_safe unless params[:email].nil?
end
end
private
def password_reset_mailer(user)
token = generate_token(user.id, user.email)
UserMailer.forgot_password(user.email, token).deliver
end
def generate_token(id, email)
hash = Digest::MD5.hexdigest(email)
"#{id}-#{hash}"
end
def is_valid?(token)
if token =~ /(?<user>\d+)-(?<email_hash>[A-Z0-9]{32})/i
# Fetch the user by their id, and hash their email address
@user = User.find_by(id: $~[:user])
email = Digest::MD5.hexdigest(@user.email)
# Compare and validate our hashes
return true if email == $~[:email_hash]
end
end
end
+44
View File
@@ -0,0 +1,44 @@
# frozen_string_literal: true
class PayController < ApplicationController
def index
end
def update_dd_info
msg = false
pay = Pay.new(
bank_account_num: params[:bank_account_num],
bank_routing_num: params[:bank_routing_num],
percent_of_deposit: params[:dd_percent],
user_id: current_user.id
)
msg = true if pay.save!
respond_to do |format|
format.json { render json: {msg: msg } }
end
end
def show
respond_to do |format|
format.json { render json: {user: current_user.pay.as_json} }
end
end
def destroy
pay = Pay.find_by_id(params[:id])
if pay.present? and pay.destroy
flash[:success] = "Successfully Deleted Entry"
else
flash[:error] = "Unable to process that request at this time"
end
redirect_to user_pay_index_path
end
def decrypted_bank_acct_num
decrypted = Encryption.decrypt_sensitive_value(params[:value_to_decrypt])
respond_to do |format|
format.json { render json: {account_num: decrypted || "No Data" } }
end
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class PerformanceController < ApplicationController
def index
@perf = current_user.performance
end
end
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class RetirementController < ApplicationController
def index
@info = current_user.retirement
end
end
+64
View File
@@ -0,0 +1,64 @@
# frozen_string_literal: true
class ScheduleController < ApplicationController
def create
message = false
if params[:schedule][:event_type] == "pto"
sched = Schedule.new(schedule_params)
sched.date_begin, sched.date_end = format_schedule_date(params[:date_range1])
sched.user_id = current_user.id
a = sched.date_end
if sched.save
message = true
end
end
respond_to do |format|
format.json { render json: {msg: message ? "success" : "failure" } }
end
end
def get_pto_schedule
begin
schedules = current_user.paid_time_off.schedule
jfs = []
schedules.each do |s|
hash = Hash.new
hash[:id] = s[:id]
hash[:title] = s[:event_name]
hash[:start] = s[:date_begin]
hash[:end] = s[:date_end]
jfs << hash
end
rescue
end
respond_to do |format|
format.json { render json: jfs.to_json }
end
end
private
# Returns a two part array consisting of dates
# First value is the begin date and the second is the end date
def format_schedule_date(date_array)
begin
vals = []
return vals if date_array.empty?
date_array.split("-").each do |s|
date = Date.strptime(s.strip, "%m/%d/%Y")
vals <<(date)
end
rescue ArgumentError
return []
end
return vals
end
private
def schedule_params
params.require(:schedule).permit(:date_begin, :date_end, :event_desc, :event_name, :event_type)
end
end
+38
View File
@@ -0,0 +1,38 @@
# frozen_string_literal: true
class SessionsController < ApplicationController
skip_before_action :has_info
skip_before_action :authenticated, only: [:new, :create]
def new
@url = params[:url]
redirect_to home_dashboard_index_path if current_user
end
def create
path = params[:url].present? ? params[:url] : home_dashboard_index_path
begin
# Normalize the email address, why not
user = User.authenticate(params[:email].to_s.strip.downcase, params[:password])
rescue RuntimeError => e
# don't do ANYTHING
end
if user
if params[:remember_me]
cookies.permanent[:auth_token] = user.auth_token
else
session[:user_id] = user.id
end
redirect_to path
else
flash[:error] = e.message
render "sessions/new"
end
end
def destroy
cookies.delete(:auth_token)
reset_session
redirect_to root_path
end
end
+192
View File
@@ -0,0 +1,192 @@
# frozen_string_literal: true
class TutorialsController < ApplicationController
skip_before_action :has_info
skip_before_action :authenticated
def credentials
# Render credentials page with layout
end
# VULNERABILITY: Regular Expression Denial of Service (ReDoS)
# This endpoint demonstrates how malicious input can cause catastrophic backtracking
# in regular expressions, potentially hanging the application.
#
# In Rails 8, Regexp.timeout is set to 1 second by default, which prevents
# infinite hangs but still allows attackers to consume server resources.
#
# Tutorial: See wiki R8-A1-ReDoS for exploitation details
def redos_email
email = params[:email]
# VULNERABLE: Complex email regex with nested quantifiers
# This pattern is susceptible to catastrophic backtracking
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
is_valid = email =~ email_pattern
elapsed_time = Time.now - start_time
render json: {
valid: is_valid.present?,
time_elapsed: elapsed_time,
message: "Email validation completed"
}
rescue Regexp::TimeoutError => e
elapsed_time = Time.now - start_time
Rails.logger.warn "[SECURITY] ReDoS attempt detected - pattern: email validation, elapsed: #{elapsed_time}s"
render json: {
error: "Timeout",
message: "Email validation timed out - possible ReDoS attack",
time_elapsed: elapsed_time
}, status: :bad_request
end
end
# VULNERABILITY: ReDoS with nested quantifiers
# Even worse than the email example - this demonstrates pure nested quantifiers
# which cause exponential backtracking.
#
# Tutorial: See wiki R8-A1-ReDoS for exploitation details
def redos_username
username = params[:username]
# EXTREMELY VULNERABLE: Nested quantifiers (a+)+
# This is the canonical ReDoS example
username_pattern = /^(a+)+$/
begin
start_time = Time.now
is_valid = username =~ username_pattern
elapsed_time = Time.now - start_time
render json: {
valid: is_valid.present?,
time_elapsed: elapsed_time,
message: "Username validation completed"
}
rescue Regexp::TimeoutError => e
elapsed_time = Time.now - start_time
Rails.logger.warn "[SECURITY] ReDoS attempt detected - pattern: username validation, elapsed: #{elapsed_time}s"
render json: {
error: "Timeout",
message: "Username validation timed out - possible ReDoS attack",
time_elapsed: elapsed_time
}, status: :bad_request
end
end
# SECURE: Fixed version using simpler regex
# This shows the proper way to validate without ReDoS risk
def redos_email_safe
email = params[:email]
# SAFE: Use Ruby's built-in URI email regex or simple validation
begin
start_time = Time.now
is_valid = email =~ URI::MailTo::EMAIL_REGEXP
elapsed_time = Time.now - start_time
render json: {
valid: is_valid.present?,
time_elapsed: elapsed_time,
message: "Email validation completed (safe method)"
}
rescue Regexp::TimeoutError => e
# This should never happen with the built-in regex, but handle it anyway
elapsed_time = Time.now - start_time
render json: {
error: "Timeout",
message: "Validation timed out",
time_elapsed: elapsed_time
}, status: :bad_request
end
end
# VULNERABILITY A03:2025 - Software Supply Chain Failures
# This endpoint demonstrates various supply chain security issues
#
# Tutorial: See wiki for A03 exploitation details
def supply_chain
render json: {
vulnerabilities: [
{
type: "Missing Subresource Integrity (SRI)",
location: "app/views/layouts/application.html.erb",
description: "CDN assets loaded without integrity checks",
impact: "If CDN is compromised, malicious code can be injected",
cve_example: "Similar to British Airways breach (2018) via Magecart"
},
{
type: "Outdated Dependencies",
location: "Gemfile.lock",
description: "Application may use gems with known vulnerabilities",
impact: "Exploitable CVEs in dependencies",
mitigation: "Run 'bundle audit' to check for known vulnerabilities"
},
{
type: "No Dependency Integrity Validation",
location: "Gemfile / bundler configuration",
description: "Gemfile.lock can be modified without detection",
impact: "Malicious dependencies could be injected",
mitigation: "Use checksums, verify signatures, implement SBOM"
},
{
type: "Insecure Gem Sources",
location: "Gemfile (if misconfigured)",
description: "Using HTTP instead of HTTPS for gem sources",
impact: "Man-in-the-middle attacks during bundle install",
note: "RailsGoat correctly uses HTTPS, but many apps don't"
},
{
type: "No Software Bill of Materials (SBOM)",
location: "Project root",
description: "Missing SBOM documentation",
impact: "Cannot track supply chain components or vulnerabilities",
mitigation: "Generate SBOM using CycloneDX or SPDX formats"
}
],
demo: "Check application.html.erb for CDN assets without SRI",
secure_example: {
vulnerable: '<script src="https://cdn.example.com/lib.js"></script>',
secure: '<script src="https://cdn.example.com/lib.js" integrity="sha384-hash" crossorigin="anonymous"></script>'
}
}
end
# Demonstrate checking for vulnerable dependencies
def check_dependencies
begin
# In a real scenario, this would run bundle-audit or similar
# For demo purposes, we'll return example vulnerability data
render json: {
status: "scan_complete",
message: "This endpoint simulates dependency vulnerability scanning",
note: "Run 'bundle audit' or 'bundle-audit check' in your terminal",
example_vulnerabilities: [
{
gem: "rails",
version: "8.0.4",
advisory: "Check https://rubysec.com for any advisories",
severity: "varies"
},
{
gem: "nokogiri",
note: "Commonly has CVEs, check current version against advisories",
resources: "https://github.com/sparklemotion/nokogiri/security/advisories"
}
],
recommended_tools: [
"bundle-audit - https://github.com/rubysec/bundler-audit",
"Dependabot - https://github.com/dependabot",
"Snyk - https://snyk.io",
"OWASP Dependency-Check"
]
}
rescue => e
render json: { error: e.message }, status: :internal_server_error
end
end
end
+57
View File
@@ -0,0 +1,57 @@
# frozen_string_literal: true
class UsersController < ApplicationController
skip_before_action :has_info
skip_before_action :authenticated, only: [:new, :create]
def new
@user = User.new
end
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
redirect_to home_dashboard_index_path
else
@user = user
flash[:error] = user.errors.full_messages.to_sentence
redirect_to :signup
end
end
def account_settings
@user = current_user
end
def update
message = false
user = User.where("id = '#{params[:user][:id]}'")[0]
if user
user.update(user_params_without_password)
if params[:user][:password].present? && (params[:user][:password] == params[:user][:password_confirmation])
user.password = params[:user][:password]
end
message = true if user.save!
respond_to do |format|
format.html { redirect_to user_account_settings_path(user_id: current_user.id) }
format.json { render json: {msg: message ? "success" : "false "} }
end
else
flash[:error] = "Could not update user!"
redirect_to user_account_settings_path(user_id: current_user.id)
end
end
private
def user_params
params.require(:user).permit!
end
# unpermitted attributes are ignored in production
def user_params_without_password
params.require(:user).permit(:email, :admin, :first_name, :last_name)
end
end
+11
View File
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class WorkInfoController < ApplicationController
def index
@user = User.find_by(id: params[:user_id])
if !(@user) || @user.admin
flash[:error] = "Sorry, no user with that user id exists"
redirect_to home_dashboard_index_path
end
end
end