Initial commit
This commit is contained in:
771
skills/sinatra-security/SKILL.md
Normal file
771
skills/sinatra-security/SKILL.md
Normal file
@@ -0,0 +1,771 @@
|
||||
---
|
||||
name: sinatra-security
|
||||
description: Security best practices for Sinatra applications including input validation, CSRF protection, and authentication patterns. Use when hardening applications or conducting security reviews.
|
||||
---
|
||||
|
||||
# Sinatra Security Skill
|
||||
|
||||
## Tier 1: Quick Reference - Essential Security
|
||||
|
||||
### CSRF Protection
|
||||
|
||||
```ruby
|
||||
# Enable Rack::Protection
|
||||
use Rack::Protection
|
||||
|
||||
# Or specifically CSRF
|
||||
use Rack::Protection::AuthenticityToken
|
||||
```
|
||||
|
||||
### XSS Prevention
|
||||
|
||||
```ruby
|
||||
# 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
|
||||
|
||||
```ruby
|
||||
# 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
|
||||
|
||||
```ruby
|
||||
use Rack::Session::Cookie,
|
||||
secret: ENV['SESSION_SECRET'], # Long random string
|
||||
same_site: :strict,
|
||||
httponly: true,
|
||||
secure: production?
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```ruby
|
||||
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
|
||||
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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
|
||||
"<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:**
|
||||
```erb
|
||||
<form method="POST" action="/users">
|
||||
<%= csrf_tag %>
|
||||
<!-- form fields -->
|
||||
</form>
|
||||
```
|
||||
|
||||
**For AJAX:**
|
||||
```javascript
|
||||
// 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:**
|
||||
```ruby
|
||||
# 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:**
|
||||
```ruby
|
||||
# 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:**
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
# 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:**
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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
|
||||
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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:
|
||||
|
||||
1. **Injection Prevention**
|
||||
- SQL Injection
|
||||
- Command Injection
|
||||
- LDAP Injection
|
||||
- XML Injection
|
||||
|
||||
2. **Broken Authentication**
|
||||
- Password policies
|
||||
- Session management
|
||||
- Multi-factor authentication
|
||||
- Account lockout
|
||||
|
||||
3. **Sensitive Data Exposure**
|
||||
- Encryption at rest
|
||||
- Encryption in transit (HTTPS)
|
||||
- Secure key storage
|
||||
- Data minimization
|
||||
|
||||
4. **XML External Entities (XXE)**
|
||||
- XML parser configuration
|
||||
- Disable external entity processing
|
||||
|
||||
5. **Broken Access Control**
|
||||
- Authentication on all protected routes
|
||||
- Authorization checks
|
||||
- IDOR prevention
|
||||
- CORS configuration
|
||||
|
||||
6. **Security Misconfiguration**
|
||||
- Remove default credentials
|
||||
- Disable directory listing
|
||||
- Error message handling
|
||||
- Keep dependencies updated
|
||||
|
||||
7. **Cross-Site Scripting (XSS)**
|
||||
- Output encoding
|
||||
- Input validation
|
||||
- Content Security Policy
|
||||
- HTTPOnly cookies
|
||||
|
||||
8. **Insecure Deserialization**
|
||||
- Validate serialized data
|
||||
- Use safe serialization formats
|
||||
- Sign serialized data
|
||||
|
||||
9. **Using Components with Known Vulnerabilities**
|
||||
- Regular dependency updates
|
||||
- Security audits (bundle audit)
|
||||
- Monitor CVE databases
|
||||
|
||||
10. **Insufficient Logging & Monitoring**
|
||||
- Log security events
|
||||
- Monitor for attacks
|
||||
- Alerting systems
|
||||
- Log rotation and retention
|
||||
|
||||
### Security Testing Examples
|
||||
|
||||
**Testing Authentication:**
|
||||
```ruby
|
||||
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:**
|
||||
```ruby
|
||||
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`
|
||||
Reference in New Issue
Block a user