sinatra-security

Sinatra Security Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "sinatra-security" with this command: npx skills add geoffjay/claude-plugins/geoffjay-claude-plugins-sinatra-security

Sinatra Security Skill

Tier 1: Quick Reference - Essential Security

CSRF Protection

Enable Rack::Protection

use Rack::Protection

Or specifically CSRF

use Rack::Protection::AuthenticityToken

XSS Prevention

In ERB templates - always escape by default

<%= user.bio %> # Escaped (safe) <%== user.bio %> # Raw (dangerous!)

In JSON responses - use proper JSON encoding

require 'json' json({ name: user.name }.to_json)

SQL Injection Prevention

BAD: String interpolation

DB["SELECT * FROM users WHERE email = '#{email}'"]

GOOD: Parameterized queries

DB["SELECT * FROM users WHERE email = ?", email]

GOOD: Hash conditions

User.where(email: email)

Secure Sessions

use Rack::Session::Cookie, secret: ENV['SESSION_SECRET'], # Long random string same_site: :strict, httponly: true, secure: production?

Input Validation

helpers do def validate_email(email) email.to_s.match?(/\A[\w+-.]+@[a-z\d-]+(.[a-z\d-]+)*.[a-z]+\z/i) end

def validate_integer(value) Integer(value) rescue ArgumentError, TypeError nil end end

post '/users' do halt 400, 'Invalid email' unless validate_email(params[:email])

Process...

end

Authentication Check

helpers do def authenticate! halt 401, json({ error: 'Unauthorized' }) unless current_user end

def current_user @current_user ||= User.find_by(id: session[:user_id]) end end

before '/admin/*' do authenticate! end

Tier 2: Detailed Instructions - Security Implementation

Comprehensive CSRF Protection

Configuration:

class Application < Sinatra::Base

Enable CSRF protection

use Rack::Protection::AuthenticityToken, except: [:json], # Skip for JSON APIs with token auth allow_if: -> (env) { # Skip for API endpoints with bearer token env['HTTP_AUTHORIZATION']&.start_with?('Bearer ') }

Manual CSRF token generation

helpers do def csrf_token session[:csrf] ||= SecureRandom.hex(32) end

def csrf_tag
  "&#x3C;input type='hidden' name='authenticity_token' value='#{csrf_token}'>"
end

def verify_csrf_token
  token = params[:authenticity_token] || request.env['HTTP_X_CSRF_TOKEN']
  halt 403, 'Invalid CSRF token' unless token == session[:csrf]
end

end

Include in forms

post '/users' do verify_csrf_token unless request.content_type == 'application/json' # Process... end end

In Views:

<form method="POST" action="/users"> <%= csrf_tag %> <!-- form fields --> </form>

For AJAX:

// Include CSRF token in AJAX requests fetch('/users', { method: 'POST', headers: { 'X-CSRF-Token': document.querySelector('[name=csrf_token]').value, 'Content-Type': 'application/json' }, body: JSON.stringify(data) });

XSS Prevention Strategies

Template Escaping:

ERB - escape by default

<div><%= user_input %></div>

Explicitly raw (only for trusted content)

<div><%== trusted_html %></div>

Sanitize user HTML

require 'sanitize'

helpers do def sanitize_html(html) Sanitize.fragment(html, Sanitize::Config::RELAXED) end end

In template

<div><%= sanitize_html(user_bio) %></div>

JSON Responses:

Always use proper JSON encoding

get '/api/users/:id' do user = User.find(params[:id])

BAD: Manual JSON construction

"{ "name": "#{user.name}" }" # XSS if name contains quotes

GOOD: Use JSON library

content_type :json { name: user.name, bio: user.bio }.to_json end

Content Security Policy:

class Application < Sinatra::Base before do headers 'Content-Security-Policy' => [ "default-src 'self'", "script-src 'self' https://cdn.example.com", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "font-src 'self'", "connect-src 'self'", "frame-ancestors 'none'" ].join('; ') end end

SQL Injection Prevention

Parameterized Queries:

Sequel

BAD

DB["SELECT * FROM users WHERE name = '#{name}'"]

GOOD

DB["SELECT * FROM users WHERE name = ?", name] DB["SELECT * FROM users WHERE name = :name", name: name]

ActiveRecord

BAD

User.where("email = '#{email}'")

GOOD

User.where(email: email) User.where("email = ?", email) User.where("email = :email", email: email)

Input Validation:

helpers do def validate_sql_param(param, type: :string) case type when :integer Integer(param) when :boolean [true, 'true', '1', 1].include?(param) when :string param.to_s.gsub(/['";\]/, '') # Remove dangerous chars else param end rescue ArgumentError halt 400, 'Invalid parameter' end end

get '/users/:id' do id = validate_sql_param(params[:id], type: :integer) user = User.find(id) json user.to_hash end

Authentication Patterns

Password Authentication:

require 'bcrypt'

class User include BCrypt

def password=(new_password) @password_hash = Password.create(new_password) end

def password_hash @password_hash end

def authenticate(password) Password.new(password_hash) == password end end

Registration

post '/register' do user = User.new( email: params[:email], name: params[:name] ) user.password = params[:password] user.save

session[:user_id] = user.id redirect '/dashboard' end

Login

post '/login' do user = User.find_by(email: params[:email])

if user&.authenticate(params[:password]) session[:user_id] = user.id session[:logged_in_at] = Time.now.to_i

redirect '/dashboard'

else halt 401, 'Invalid credentials' end end

Token-Based Authentication:

require 'jwt'

class TokenAuth SECRET = ENV['JWT_SECRET']

def self.encode(payload, exp = 24.hours.from_now) payload[:exp] = exp.to_i JWT.encode(payload, SECRET, 'HS256') end

def self.decode(token) body = JWT.decode(token, SECRET, true, algorithm: 'HS256')[0] HashWithIndifferentAccess.new(body) rescue JWT::DecodeError, JWT::ExpiredSignature nil end end

Middleware

class JWTAuth def initialize(app) @app = app end

def call(env) auth_header = env['HTTP_AUTHORIZATION'] token = auth_header&.split(' ')&.last

if payload = TokenAuth.decode(token)
  env['current_user_id'] = payload[:user_id]
  @app.call(env)
else
  [401, { 'Content-Type' => 'application/json' },
   ['{"error": "Unauthorized"}']]
end

end end

Login endpoint

post '/api/login' do user = User.find_by(email: params[:email])

if user&.authenticate(params[:password]) token = TokenAuth.encode(user_id: user.id) json({ token: token, user: user.to_hash }) else halt 401, json({ error: 'Invalid credentials' }) end end

Protected routes

class API < Sinatra::Base use JWTAuth

helpers do def current_user @current_user ||= User.find(request.env['current_user_id']) end end

get '/profile' do json current_user.to_hash end end

API Key Authentication:

class APIKeyAuth def initialize(app) @app = app end

def call(env) api_key = env['HTTP_X_API_KEY']

if valid_api_key?(api_key)
  user = User.find_by(api_key: api_key)
  env['current_user'] = user
  @app.call(env)
else
  [401, { 'Content-Type' => 'application/json' },
   ['{"error": "Invalid API key"}']]
end

end

private

def valid_api_key?(key) key && User.exists?(api_key: key, active: true) end end

use APIKeyAuth

Generate API keys

helpers do def generate_api_key SecureRandom.hex(32) end end

post '/api/keys' do authenticate! api_key = generate_api_key current_user.update(api_key: api_key) json({ api_key: api_key }) end

Authorization Patterns

Role-Based Access Control:

class User ROLES = [:guest, :user, :admin, :superadmin]

def has_role?(role) ROLES.index(self.role) >= ROLES.index(role) end

def can?(action, resource) case role when :admin, :superadmin true when :user action == :read || resource.user_id == id else action == :read end end end

helpers do def authorize!(action, resource) unless current_user&.can?(action, resource) halt 403, json({ error: 'Forbidden' }) end end end

Usage

get '/posts/:id' do post = Post.find(params[:id]) authorize!(:read, post) json post.to_hash end

delete '/posts/:id' do post = Post.find(params[:id]) authorize!(:delete, post) post.destroy status 204 end

Permission-Based Authorization:

class Permission ACTIONS = { posts: [:create, :read, :update, :delete], users: [:read, :update, :delete], comments: [:create, :read, :delete] }

def self.check(user, action, resource_type) return false unless user

permissions = user.permissions
permissions.include?("#{resource_type}:#{action}") ||
  permissions.include?("#{resource_type}:*") ||
  permissions.include?("*:*")

end end

helpers do def can?(action, resource_type) Permission.check(current_user, action, resource_type) end

def authorize!(action, resource_type) unless can?(action, resource_type) halt 403, json({ error: 'Forbidden' }) end end end

post '/posts' do authorize!(:create, :posts)

Create post

end

Rate Limiting

Using Rack::Attack:

require 'rack/attack'

class Rack::Attack

Throttle login attempts

throttle('login/ip', limit: 5, period: 60) do |req| req.ip if req.path == '/login' && req.post? end

Throttle API requests by API key

throttle('api/key', limit: 100, period: 60) do |req| req.env['HTTP_X_API_KEY'] if req.path.start_with?('/api') end

Throttle by IP

throttle('req/ip', limit: 300, period: 60) do |req| req.ip end

Block known bad actors

blocklist('block bad IPs') do |req| BadIP.blocked?(req.ip) end

Custom response

self.throttled_responder = lambda do |env| [ 429, { 'Content-Type' => 'application/json' }, [{ error: 'Rate limit exceeded' }.to_json] ] end end

use Rack::Attack

Secure File Uploads

require 'securerandom'

class FileUploadHandler ALLOWED_TYPES = { 'image/jpeg' => '.jpg', 'image/png' => '.png', 'image/gif' => '.gif', 'application/pdf' => '.pdf' }

MAX_SIZE = 5 * 1024 * 1024 # 5MB

def self.process(file) # Validate file presence return { error: 'No file provided' } unless file

# Validate file size
if file[:tempfile].size > MAX_SIZE
  return { error: 'File too large' }
end

# Validate content type
content_type = file[:type]
unless ALLOWED_TYPES.key?(content_type)
  return { error: 'Invalid file type' }
end

# Sanitize filename
original_name = File.basename(file[:filename])
sanitized_name = original_name.gsub(/[^a-zA-Z0-9\._-]/, '')

# Generate unique filename
extension = ALLOWED_TYPES[content_type]
unique_name = "#{SecureRandom.hex(16)}#{extension}"

# Save file
upload_dir = 'uploads'
FileUtils.mkdir_p(upload_dir)
path = File.join(upload_dir, unique_name)

File.open(path, 'wb') do |f|
  f.write(file[:tempfile].read)
end

{ success: true, path: path, filename: unique_name }

end end

post '/upload' do result = FileUploadHandler.process(params[:file])

if result[:error] halt 400, json({ error: result[:error] }) else json({ url: "/uploads/#{result[:filename]}" }) end end

Tier 3: Resources & Examples

Security Headers

Comprehensive Security Headers:

class SecurityHeaders HEADERS = { 'X-Frame-Options' => 'DENY', 'X-Content-Type-Options' => 'nosniff', 'X-XSS-Protection' => '1; mode=block', 'Referrer-Policy' => 'strict-origin-when-cross-origin', 'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()', 'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains' }

def initialize(app) @app = app end

def call(env) status, headers, body = @app.call(env) headers.merge!(HEADERS) [status, headers, body] end end

use SecurityHeaders

OWASP Security Checklist

See assets/owasp-checklist.md for complete checklist covering:

Injection Prevention

  • SQL Injection

  • Command Injection

  • LDAP Injection

  • XML Injection

Broken Authentication

  • Password policies

  • Session management

  • Multi-factor authentication

  • Account lockout

Sensitive Data Exposure

  • Encryption at rest

  • Encryption in transit (HTTPS)

  • Secure key storage

  • Data minimization

XML External Entities (XXE)

  • XML parser configuration

  • Disable external entity processing

Broken Access Control

  • Authentication on all protected routes

  • Authorization checks

  • IDOR prevention

  • CORS configuration

Security Misconfiguration

  • Remove default credentials

  • Disable directory listing

  • Error message handling

  • Keep dependencies updated

Cross-Site Scripting (XSS)

  • Output encoding

  • Input validation

  • Content Security Policy

  • HTTPOnly cookies

Insecure Deserialization

  • Validate serialized data

  • Use safe serialization formats

  • Sign serialized data

Using Components with Known Vulnerabilities

  • Regular dependency updates

  • Security audits (bundle audit)

  • Monitor CVE databases

Insufficient Logging & Monitoring

  • Log security events

  • Monitor for attacks

  • Alerting systems

  • Log rotation and retention

Security Testing Examples

Testing Authentication:

RSpec.describe 'Authentication' do describe 'POST /login' do let(:user) { create(:user, email: 'test@example.com', password: 'password123') }

it 'succeeds with valid credentials' do
  post '/login', { email: 'test@example.com', password: 'password123' }.to_json,
    'CONTENT_TYPE' => 'application/json'

  expect(last_response).to be_ok
  expect(json_response).to have_key('token')
end

it 'fails with invalid password' do
  post '/login', { email: 'test@example.com', password: 'wrong' }.to_json,
    'CONTENT_TYPE' => 'application/json'

  expect(last_response.status).to eq(401)
end

it 'prevents brute force attacks' do
  6.times do
    post '/login', { email: 'test@example.com', password: 'wrong' }.to_json,
      'CONTENT_TYPE' => 'application/json'
  end

  expect(last_response.status).to eq(429)  # Rate limited
end

end end

Testing Authorization:

RSpec.describe 'Authorization' do let(:user) { create(:user) } let(:admin) { create(:user, role: :admin) } let(:post) { create(:post, user: user) }

describe 'DELETE /posts/:id' do it 'allows owner to delete' do delete "/posts/#{post.id}", {}, auth_header(user.token) expect(last_response.status).to eq(204) end

it 'allows admin to delete' do
  delete "/posts/#{post.id}", {}, auth_header(admin.token)
  expect(last_response.status).to eq(204)
end

it 'denies other users' do
  other_user = create(:user)
  delete "/posts/#{post.id}", {}, auth_header(other_user.token)
  expect(last_response.status).to eq(403)
end

it 'requires authentication' do
  delete "/posts/#{post.id}"
  expect(last_response.status).to eq(401)
end

end end

Additional Resources

  • Security Middleware: assets/security-middleware.rb

  • Authentication Patterns: assets/auth-patterns.rb

  • OWASP Checklist: assets/owasp-checklist.md

  • Security Audit Template: references/security-audit-template.md

  • Penetration Testing Guide: references/penetration-testing.md

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

documentation-update

No summary provided by upstream source.

Repository SourceNeeds Review
General

git-troubleshooting

No summary provided by upstream source.

Repository SourceNeeds Review
General

git-advanced

No summary provided by upstream source.

Repository SourceNeeds Review