This commit is contained in:
Executable
+78
@@ -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
|
||||
Executable
+61
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
class RetirementController < ApplicationController
|
||||
|
||||
def index
|
||||
@info = current_user.retirement
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
Executable
+38
@@ -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
|
||||
Executable
+192
@@ -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
|
||||
Executable
+57
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user