Overview
API security encompasses the strategies, techniques, and controls that protect APIs from unauthorized access, data breaches, and malicious attacks. As APIs serve as the primary interface for modern applications to communicate with backend services and external systems, they represent critical attack surfaces that require defense at multiple layers.
The core challenge in API security stems from the tension between accessibility and protection. APIs must remain accessible to legitimate clients while blocking malicious actors. This balance requires implementing multiple security controls that work together: authentication verifies client identity, authorization controls resource access, encryption protects data in transit, rate limiting prevents abuse, and input validation blocks injection attacks.
API security differs from traditional web application security in several ways. APIs typically lack user interfaces, making CSRF tokens and same-origin policies less applicable. APIs often serve machine clients rather than human users, requiring different authentication mechanisms. APIs frequently expose more granular operations than web pages, increasing the attack surface. APIs may need to support multiple client types simultaneously, each with different security requirements.
The consequences of API security failures extend beyond data breaches. Compromised APIs can enable attackers to manipulate business logic, access unauthorized resources, perform privilege escalation, and launch attacks against other systems. The 2019 OWASP API Security Top 10 identified broken object level authorization, broken user authentication, and excessive data exposure as the most critical API security risks.
# Insecure API endpoint without authentication
get '/users/:id' do
user = User.find(params[:id])
user.to_json
end
# Secure API endpoint with authentication and authorization
get '/users/:id' do
authenticate_request! # Verify valid token
user = User.find(params[:id])
authorize! :read, user # Check permission
user.to_json(only: [:id, :name, :email]) # Limit exposed fields
end
Key Principles
Authentication and Identity Verification establishes who the client is before granting access. APIs should require authentication for all endpoints except those explicitly designed for public access. Token-based authentication using JWT or OAuth 2.0 provides stateless verification without session storage. API keys offer simpler authentication for service-to-service communication but require secure storage and rotation. Multi-factor authentication adds defense in depth for sensitive operations.
Authorization and Access Control determines what authenticated clients can access. Role-based access control (RBAC) assigns permissions based on user roles. Attribute-based access control (ABAC) makes decisions based on attributes of the user, resource, and environment. Object-level authorization verifies permission for each specific resource, preventing horizontal privilege escalation where users access data belonging to other users at the same privilege level. Function-level authorization prevents vertical privilege escalation by restricting administrative operations.
Data Protection and Encryption secures information throughout its lifecycle. Transport encryption using TLS 1.2 or higher protects data in transit from eavesdropping and tampering. APIs should enforce HTTPS exclusively and reject HTTP connections. Sensitive data in responses requires additional protection through field-level encryption or tokenization. Encryption at rest protects stored data from unauthorized access. APIs should minimize data exposure by returning only necessary fields and implementing field-level access control.
Input Validation and Sanitization defends against injection attacks and data corruption. APIs must validate all input parameters against expected types, formats, ranges, and patterns. Whitelist validation accepts only known-good values. String inputs require length limits and character set restrictions. Structured data needs schema validation. SQL injection, command injection, and XML external entity attacks all exploit insufficient input validation. Content-Type validation ensures the request body matches declared content type.
Rate Limiting and Throttling prevent abuse and ensure availability. Rate limits restrict the number of requests from a client within a time window. Throttling slows excessive requests without blocking them entirely. Different endpoints may require different rate limits based on computational cost and sensitivity. Rate limiting protects against brute force attacks, denial of service, and resource exhaustion. Implementation requires tracking request counts per client identifier across distributed systems.
Error Handling and Information Disclosure prevents leaking sensitive information through error messages. APIs should return generic error messages to clients while logging detailed errors internally. Stack traces, database errors, and filesystem paths expose system internals that aid attackers. HTTP status codes should match the actual error type. Rate limit errors should not reveal whether authentication succeeded. Timing attacks exploit variations in response times to infer system behavior.
Security Headers and CORS Policy configure browser security controls. The Content-Security-Policy header restricts resource loading. X-Content-Type-Options prevents MIME sniffing. Strict-Transport-Security enforces HTTPS. Cross-Origin Resource Sharing (CORS) headers control which domains can access the API from browsers. Overly permissive CORS policies allow attacks from malicious websites. The Access-Control-Allow-Origin header should specify exact allowed origins rather than using wildcards.
API Versioning and Deprecation maintains security as APIs evolve. Versioning allows removing insecure functionality without breaking existing clients. Deprecated endpoints should return warnings and eventual removal dates. Security patches may require forced migration to new versions. Version identifiers in URLs or headers indicate which API contract applies.
Ruby Implementation
Ruby APIs commonly use Rack-based frameworks like Rails, Sinatra, or Grape. Authentication typically relies on JSON Web Tokens with libraries like jwt or OAuth 2.0 with doorkeeper. Authorization uses pundit or cancancan for policy enforcement. Rate limiting employs rack-attack for request throttling.
Token-Based Authentication generates and verifies JWT tokens for stateless authentication. The token contains claims about the user and expires after a set duration. Tokens include signatures to prevent tampering.
require 'jwt'
class TokenAuth
SECRET_KEY = ENV['JWT_SECRET_KEY']
ALGORITHM = 'HS256'
def self.encode(payload, expiration = 24.hours.from_now)
payload[:exp] = expiration.to_i
JWT.encode(payload, SECRET_KEY, ALGORITHM)
end
def self.decode(token)
decoded = JWT.decode(token, SECRET_KEY, true, algorithm: ALGORITHM)
HashWithIndifferentAccess.new(decoded[0])
rescue JWT::ExpiredSignature
raise AuthenticationError, 'Token expired'
rescue JWT::DecodeError
raise AuthenticationError, 'Invalid token'
end
end
# Usage in API endpoint
class ApiController < ActionController::API
before_action :authenticate_request
private
def authenticate_request
token = request.headers['Authorization']&.split(' ')&.last
raise AuthenticationError, 'Missing token' unless token
@current_user_claims = TokenAuth.decode(token)
@current_user = User.find(@current_user_claims[:user_id])
rescue AuthenticationError => e
render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
Authorization with Pundit separates authorization logic into policy objects. Each resource type has a policy class defining permitted operations. Policies receive the current user and resource as arguments.
# app/policies/article_policy.rb
class ArticlePolicy
attr_reader :user, :article
def initialize(user, article)
@user = user
@article = article
end
def show?
article.published? || article.author == user || user.admin?
end
def update?
article.author == user || user.admin?
end
def destroy?
user.admin? || (article.author == user && article.created_at > 1.hour.ago)
end
end
# app/controllers/articles_controller.rb
class ArticlesController < ApiController
def show
@article = Article.find(params[:id])
authorize @article
render json: @article
end
def update
@article = Article.find(params[:id])
authorize @article
if @article.update(article_params)
render json: @article
else
render json: { errors: @article.errors }, status: :unprocessable_entity
end
end
end
Input Validation with Strong Parameters filters and validates request parameters. Rails strong parameters whitelist permitted attributes, preventing mass assignment vulnerabilities.
class UsersController < ApiController
def create
user_params = validate_user_params
user = User.new(user_params)
if user.save
render json: user, status: :created
else
render json: { errors: user.errors }, status: :unprocessable_entity
end
end
private
def validate_user_params
params.require(:user).permit(:email, :name, :password, :password_confirmation)
end
end
# Additional validation with custom constraints
class UserCreateParams
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :email, :name, :password, :age
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
validates :password, presence: true, length: { minimum: 12 }
validates :age, numericality: { greater_than_or_equal_to: 13, less_than: 150 }
def self.from_params(params)
new(
email: params[:email],
name: params[:name],
password: params[:password],
age: params[:age]
)
end
end
Rate Limiting with Rack::Attack throttles requests based on client identifiers. Configuration specifies limits per endpoint and time window. Throttles apply to IP addresses or authenticated users.
# config/initializers/rack_attack.rb
class Rack::Attack
# Throttle general API requests by IP
throttle('api/ip', limit: 100, period: 1.minute) do |req|
req.ip if req.path.start_with?('/api/')
end
# Throttle login attempts by email
throttle('logins/email', limit: 5, period: 20.minutes) do |req|
if req.path == '/api/login' && req.post?
req.params['email'].to_s.downcase.presence
end
end
# Throttle authenticated users by user ID
throttle('api/user', limit: 300, period: 1.minute) do |req|
if req.path.start_with?('/api/') && req.env['current_user']
req.env['current_user'].id
end
end
# Block requests from suspicious IPs
blocklist('block suspicious ips') do |req|
SuspiciousIp.where(ip: req.ip).exists?
end
# Custom response for throttled requests
self.throttled_responder = lambda do |env|
retry_after = env['rack.attack.match_data'][:period]
[
429,
{'Content-Type' => 'application/json', 'Retry-After' => retry_after.to_s},
[{error: 'Rate limit exceeded', retry_after: retry_after}.to_json]
]
end
end
Secure Password Storage hashes passwords with bcrypt before storage. Bcrypt includes salt generation and cost factors to resist brute force attacks.
class User < ApplicationRecord
has_secure_password
validates :password, length: { minimum: 12 }, on: :create
validates :password, format: {
with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/,
message: 'must include uppercase, lowercase, number, and special character'
}, on: :create
end
# Manual password hashing with bcrypt
require 'bcrypt'
class PasswordService
COST = 12 # Higher cost increases computation time
def self.hash_password(password)
BCrypt::Password.create(password, cost: COST)
end
def self.verify_password(password, hash)
BCrypt::Password.new(hash) == password
end
end
HTTPS Enforcement redirects HTTP requests to HTTPS and sets security headers. Rails provides configuration options for forcing SSL connections.
# config/environments/production.rb
Rails.application.configure do
config.force_ssl = true
config.ssl_options = {
redirect: {
exclude: -> request { request.path.start_with?('/health') }
},
hsts: {
expires: 1.year,
subdomains: true,
preload: true
}
}
end
# Custom middleware for additional security headers
class SecurityHeaders
def initialize(app)
@app = app
end
def call(env)
status, headers, response = @app.call(env)
headers['X-Content-Type-Options'] = 'nosniff'
headers['X-Frame-Options'] = 'DENY'
headers['X-XSS-Protection'] = '1; mode=block'
headers['Content-Security-Policy'] = "default-src 'self'"
headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
[status, headers, response]
end
end
Common Patterns
OAuth 2.0 Authorization separates authentication from resource access. Clients obtain access tokens from an authorization server, then present tokens to resource servers. The authorization code flow provides the most secure option for server-side applications.
# Using Doorkeeper gem for OAuth 2.0 provider
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
resource_owner_authenticator do
User.find_by(id: session[:user_id]) || redirect_to(login_url)
end
admin_authenticator do
User.find_by(id: session[:user_id])&.admin? || redirect_to(login_url)
end
access_token_expires_in 2.hours
refresh_token_enabled true
grant_flows %w[authorization_code client_credentials]
# Require PKCE for authorization code grant
force_ssl_in_redirect_uri true
end
# Protected resource endpoint
class ApiController < ApplicationController
before_action :doorkeeper_authorize!
def index
current_user = User.find(doorkeeper_token.resource_owner_id)
render json: { data: current_user.resources }
end
private
def doorkeeper_unauthorized_render_options(error:)
{ json: { error: error.description } }
end
end
API Key Authentication uses secret keys for service-to-service authentication. API keys should include sufficient entropy, rotate regularly, and scope to specific permissions.
class ApiKey < ApplicationRecord
belongs_to :user
before_create :generate_key
validates :name, presence: true
validates :key_hash, uniqueness: true
scope :active, -> { where(revoked_at: nil) }
def self.authenticate(key)
key_hash = hash_key(key)
api_key = active.find_by(key_hash: key_hash)
api_key&.touch(:last_used_at)
api_key
end
def revoke!
update(revoked_at: Time.current)
end
private
def generate_key
key = SecureRandom.hex(32)
self.key_hash = self.class.hash_key(key)
self.key_prefix = key[0..7]
key # Return key only at creation time
end
def self.hash_key(key)
Digest::SHA256.hexdigest(key)
end
end
# API key authentication middleware
class ApiKeyAuth
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
api_key = extract_api_key(request)
if api_key && (key_record = ApiKey.authenticate(api_key))
env['current_api_key'] = key_record
env['current_user'] = key_record.user
end
@app.call(env)
end
private
def extract_api_key(request)
request.env['HTTP_X_API_KEY'] || request.params['api_key']
end
end
Request Signing verifies request integrity and authenticity. Clients sign requests using a shared secret or private key. Servers validate signatures before processing requests.
class RequestSigner
def self.sign(method, path, body, timestamp, secret)
string_to_sign = [method.upcase, path, body, timestamp].join("\n")
OpenSSL::HMAC.hexdigest('SHA256', secret, string_to_sign)
end
def self.verify(request, secret, max_age = 5.minutes)
signature = request.headers['X-Signature']
timestamp = request.headers['X-Timestamp'].to_i
raise AuthenticationError, 'Missing signature' unless signature
raise AuthenticationError, 'Missing timestamp' if timestamp.zero?
raise AuthenticationError, 'Request too old' if Time.at(timestamp) < max_age.ago
expected_signature = sign(
request.method,
request.path,
request.body.read,
timestamp,
secret
)
ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
end
end
# Usage in controller
class WebhookController < ApplicationController
skip_before_action :verify_authenticity_token
def receive
secret = ENV['WEBHOOK_SECRET']
unless RequestSigner.verify(request, secret)
render json: { error: 'Invalid signature' }, status: :unauthorized
return
end
process_webhook(params)
head :ok
end
end
Scope-Based Authorization grants permissions at granular levels. Scopes define what operations a token can perform. Token introspection reveals granted scopes.
class TokenScope
SCOPES = {
'read:users' => 'Read user data',
'write:users' => 'Modify user data',
'read:articles' => 'Read articles',
'write:articles' => 'Create and modify articles',
'delete:articles' => 'Delete articles',
'admin' => 'Administrative access'
}.freeze
def initialize(scope_string)
@scopes = scope_string.to_s.split
end
def permits?(required_scope)
@scopes.include?('admin') || @scopes.include?(required_scope)
end
def permits_all?(*required_scopes)
required_scopes.all? { |scope| permits?(scope) }
end
def permits_any?(*required_scopes)
required_scopes.any? { |scope| permits?(scope) }
end
end
# Controller authorization with scopes
class ArticlesController < ApiController
before_action :require_scopes
def index
render json: Article.all
end
def create
article = Article.new(article_params)
if article.save
render json: article, status: :created
else
render json: { errors: article.errors }, status: :unprocessable_entity
end
end
private
def require_scopes
token_scopes = TokenScope.new(@current_user_claims[:scope])
case action_name
when 'index', 'show'
unauthorized unless token_scopes.permits?('read:articles')
when 'create', 'update'
unauthorized unless token_scopes.permits?('write:articles')
when 'destroy'
unauthorized unless token_scopes.permits?('delete:articles')
end
end
def unauthorized
render json: { error: 'Insufficient permissions' }, status: :forbidden
end
end
Practical Examples
Securing a User Profile API demonstrates authentication, authorization, and data filtering. Users retrieve their own profiles or public profiles of other users. Admin users access all fields.
class ProfilesController < ApiController
before_action :authenticate_request
before_action :load_profile
before_action :authorize_access
def show
render json: filtered_profile_data
end
def update
if @profile.update(profile_params)
render json: filtered_profile_data
else
render json: { errors: @profile.errors }, status: :unprocessable_entity
end
end
private
def load_profile
@profile = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'Profile not found' }, status: :not_found
end
def authorize_access
case action_name
when 'show'
# Anyone can view public profiles
return if @profile.public?
# Users can view their own private profiles
return if @current_user.id == @profile.id
# Admins can view all profiles
return if @current_user.admin?
render json: { error: 'Access denied' }, status: :forbidden
when 'update'
unless @current_user.id == @profile.id || @current_user.admin?
render json: { error: 'Access denied' }, status: :forbidden
end
end
end
def filtered_profile_data
fields = if @current_user.admin?
[:id, :email, :name, :created_at, :last_login, :status]
elsif @current_user.id == @profile.id
[:id, :email, :name, :bio, :created_at]
else
[:id, :name, :bio]
end
@profile.as_json(only: fields)
end
def profile_params
params.require(:profile).permit(:name, :bio, :public)
end
end
Implementing Secure File Upload validates file types, limits file sizes, and prevents path traversal attacks. Uploaded files store in secure locations with random filenames.
class FileUploadController < ApiController
MAX_FILE_SIZE = 10.megabytes
ALLOWED_CONTENT_TYPES = %w[image/jpeg image/png application/pdf].freeze
def create
file = params[:file]
validate_file!(file)
upload = Upload.create!(
user: @current_user,
filename: secure_filename(file.original_filename),
content_type: file.content_type,
size: file.size
)
storage_path = generate_storage_path(upload)
File.open(storage_path, 'wb') do |f|
f.write(file.read)
end
upload.update!(storage_path: storage_path)
render json: upload, status: :created
end
private
def validate_file!(file)
raise ValidationError, 'No file provided' unless file
raise ValidationError, 'File too large' if file.size > MAX_FILE_SIZE
unless ALLOWED_CONTENT_TYPES.include?(file.content_type)
raise ValidationError, 'Invalid file type'
end
# Verify content matches declared type
detected_type = Marcel::MimeType.for(file.tempfile)
unless detected_type == file.content_type
raise ValidationError, 'File content does not match declared type'
end
end
def secure_filename(original_filename)
extension = File.extname(original_filename)
"#{SecureRandom.uuid}#{extension}"
end
def generate_storage_path(upload)
date_path = upload.created_at.strftime('%Y/%m/%d')
Rails.root.join('storage', 'uploads', date_path, upload.filename)
end
end
Building a Secure Payment API requires multiple security layers: HTTPS enforcement, request signing, idempotency keys, and audit logging.
class PaymentsController < ApiController
before_action :authenticate_request
before_action :verify_signature
before_action :check_idempotency
def create
payment = Payment.new(payment_params)
payment.user = @current_user
payment.idempotency_key = request.headers['Idempotency-Key']
if payment.save
ProcessPaymentJob.perform_later(payment.id)
log_payment_request(payment)
render json: payment, status: :created
else
render json: { errors: payment.errors }, status: :unprocessable_entity
end
rescue PaymentError => e
log_payment_error(e)
render json: { error: e.message }, status: :unprocessable_entity
end
private
def verify_signature
unless RequestSigner.verify(request, @current_user.api_secret)
render json: { error: 'Invalid signature' }, status: :unauthorized
end
end
def check_idempotency
idempotency_key = request.headers['Idempotency-Key']
unless idempotency_key
render json: { error: 'Idempotency-Key required' }, status: :bad_request
return
end
existing = Payment.find_by(
user: @current_user,
idempotency_key: idempotency_key
)
if existing
render json: existing, status: :ok
end
end
def payment_params
params.require(:payment).permit(:amount, :currency, :description)
end
def log_payment_request(payment)
AuditLog.create!(
user: @current_user,
action: 'payment_created',
resource_type: 'Payment',
resource_id: payment.id,
ip_address: request.remote_ip,
user_agent: request.user_agent,
details: { amount: payment.amount, currency: payment.currency }
)
end
def log_payment_error(error)
ErrorLog.create!(
user: @current_user,
error_type: error.class.name,
message: error.message,
backtrace: error.backtrace.first(10),
ip_address: request.remote_ip
)
end
end
Common Pitfalls
Broken Object Level Authorization occurs when APIs fail to verify that the authenticated user has permission to access a specific resource. Attackers modify object identifiers in requests to access unauthorized data.
# VULNERABLE: No authorization check
class ArticlesController < ApiController
def show
article = Article.find(params[:id])
render json: article
end
end
# SECURE: Verify ownership or permission
class ArticlesController < ApiController
def show
article = @current_user.articles.find(params[:id])
render json: article
rescue ActiveRecord::RecordNotFound
render json: { error: 'Not found' }, status: :not_found
end
end
Excessive Data Exposure happens when APIs return complete objects without filtering sensitive fields. APIs should return only necessary data based on the client's permission level.
# VULNERABLE: Returns all user fields including sensitive data
def show
user = User.find(params[:id])
render json: user
end
# SECURE: Filter fields based on context
def show
user = User.find(params[:id])
if @current_user.admin?
render json: user.as_json(except: [:password_digest])
elsif @current_user.id == user.id
render json: user.as_json(only: [:id, :email, :name, :created_at])
else
render json: user.as_json(only: [:id, :name, :public_bio])
end
end
Mass Assignment Vulnerabilities allow attackers to modify fields they should not access by including unexpected parameters in requests. Strong parameters prevent this attack.
# VULNERABLE: Accepts all parameters
def update
user = User.find(params[:id])
user.update(params[:user])
render json: user
end
# SECURE: Whitelist allowed parameters
def update
user = User.find(params[:id])
user.update(user_params)
render json: user
end
private
def user_params
allowed = [:name, :bio, :email]
allowed << :role if @current_user.admin?
params.require(:user).permit(allowed)
end
Insecure Direct Object References expose internal object identifiers that allow enumeration attacks. Using UUIDs or signed tokens prevents predictable identifier guessing.
# VULNERABLE: Sequential integer IDs
class User < ApplicationRecord
# id: 1, 2, 3, 4... (easily enumerable)
end
# SECURE: Use UUIDs as primary keys
class User < ApplicationRecord
self.primary_key = :uuid
before_create :generate_uuid
private
def generate_uuid
self.uuid = SecureRandom.uuid
end
end
# SECURE: Use signed tokens for temporary access
class ShareLink
def self.generate(resource)
payload = {
resource_type: resource.class.name,
resource_id: resource.id,
exp: 24.hours.from_now.to_i
}
TokenAuth.encode(payload)
end
def self.resolve(token)
claims = TokenAuth.decode(token)
claims[:resource_type].constantize.find(claims[:resource_id])
end
end
Missing Rate Limiting enables brute force attacks, credential stuffing, and denial of service. Rate limits should apply per endpoint based on sensitivity.
# Without rate limiting, attackers can make unlimited login attempts
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
render json: { token: TokenAuth.encode(user_id: user.id) }
else
render json: { error: 'Invalid credentials' }, status: :unauthorized
end
end
# With rate limiting (requires Rack::Attack configuration)
throttle('logins/email', limit: 5, period: 20.minutes) do |req|
if req.path == '/api/login' && req.post?
req.params['email'].to_s.downcase.presence
end
end
Insufficient Logging and Monitoring prevents detection of security incidents. APIs should log authentication attempts, authorization failures, and suspicious patterns.
class SecurityLogger
def self.log_authentication(success:, user:, ip:, user_agent:)
SecurityEvent.create!(
event_type: success ? 'auth_success' : 'auth_failure',
user_id: user&.id,
ip_address: ip,
user_agent: user_agent,
timestamp: Time.current
)
# Alert on multiple failures
if !success && recent_failures(ip) > 5
AlertService.notify_suspicious_activity(ip)
end
end
def self.recent_failures(ip)
SecurityEvent.where(
event_type: 'auth_failure',
ip_address: ip,
timestamp: 10.minutes.ago..Time.current
).count
end
end
Testing Approaches
Authentication Testing verifies that endpoints reject unauthenticated requests and accept valid tokens. Tests should cover expired tokens, invalid signatures, and missing tokens.
RSpec.describe 'Articles API', type: :request do
describe 'GET /api/articles' do
it 'rejects requests without authentication' do
get '/api/articles'
expect(response).to have_http_status(:unauthorized)
end
it 'accepts valid authentication tokens' do
user = create(:user)
token = TokenAuth.encode(user_id: user.id)
get '/api/articles', headers: { 'Authorization' => "Bearer #{token}" }
expect(response).to have_http_status(:ok)
end
it 'rejects expired tokens' do
user = create(:user)
token = TokenAuth.encode({ user_id: user.id }, 1.hour.ago)
get '/api/articles', headers: { 'Authorization' => "Bearer #{token}" }
expect(response).to have_http_status(:unauthorized)
expect(json_response['error']).to match(/expired/i)
end
it 'rejects tampered tokens' do
user = create(:user)
token = TokenAuth.encode(user_id: user.id)
tampered_token = token[0..-5] + 'XXXX'
get '/api/articles', headers: { 'Authorization' => "Bearer #{tampered_token}" }
expect(response).to have_http_status(:unauthorized)
end
end
end
Authorization Testing ensures that users can only access permitted resources. Tests verify both horizontal and vertical privilege escalation prevention.
RSpec.describe 'User Profiles API', type: :request do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:admin) { create(:user, :admin) }
describe 'GET /api/users/:id' do
it 'allows users to view their own profile' do
get "/api/users/#{user.id}", headers: auth_headers(user)
expect(response).to have_http_status(:ok)
end
it 'prevents users from viewing other private profiles' do
other_user.update(public: false)
get "/api/users/#{other_user.id}", headers: auth_headers(user)
expect(response).to have_http_status(:forbidden)
end
it 'allows viewing public profiles' do
other_user.update(public: true)
get "/api/users/#{other_user.id}", headers: auth_headers(user)
expect(response).to have_http_status(:ok)
end
it 'allows admins to view all profiles' do
other_user.update(public: false)
get "/api/users/#{other_user.id}", headers: auth_headers(admin)
expect(response).to have_http_status(:ok)
end
end
describe 'PATCH /api/users/:id' do
it 'prevents users from updating other profiles' do
patch "/api/users/#{other_user.id}",
params: { user: { name: 'Hacked' } },
headers: auth_headers(user)
expect(response).to have_http_status(:forbidden)
expect(other_user.reload.name).not_to eq('Hacked')
end
it 'prevents users from elevating their own privileges' do
patch "/api/users/#{user.id}",
params: { user: { role: 'admin' } },
headers: auth_headers(user)
expect(user.reload.role).not_to eq('admin')
end
end
end
Input Validation Testing checks that invalid inputs are rejected and do not cause errors or security issues. Tests cover SQL injection, XSS, and command injection attempts.
RSpec.describe 'Search API', type: :request do
let(:user) { create(:user) }
describe 'GET /api/search' do
it 'rejects SQL injection attempts' do
malicious_queries = [
"'; DROP TABLE users; --",
"1' OR '1'='1",
"admin'--"
]
malicious_queries.each do |query|
get '/api/search',
params: { q: query },
headers: auth_headers(user)
expect(response).to have_http_status(:ok)
expect(User.count).to be > 0 # Users table still exists
end
end
it 'rejects overly long search queries' do
long_query = 'a' * 1001
get '/api/search',
params: { q: long_query },
headers: auth_headers(user)
expect(response).to have_http_status(:bad_request)
end
it 'handles special characters safely' do
special_chars = ['<script>', '${code}', '../../../etc/passwd']
special_chars.each do |chars|
get '/api/search',
params: { q: chars },
headers: auth_headers(user)
expect(response).to have_http_status(:ok)
end
end
end
end
Rate Limiting Testing verifies that excessive requests are throttled. Tests should cover different rate limit tiers and ensure counters reset correctly.
RSpec.describe 'Rate Limiting', type: :request do
let(:user) { create(:user) }
describe 'API request throttling' do
it 'allows requests under the rate limit' do
5.times do
get '/api/articles', headers: auth_headers(user)
expect(response).to have_http_status(:ok)
end
end
it 'blocks requests exceeding the rate limit' do
# Assume limit is 10 requests per minute
11.times do |i|
get '/api/articles', headers: auth_headers(user)
if i < 10
expect(response).to have_http_status(:ok)
else
expect(response).to have_http_status(:too_many_requests)
expect(response.headers['Retry-After']).to be_present
end
end
end
it 'applies different limits to different endpoints' do
# Login endpoint has stricter limits
6.times do
post '/api/login', params: { email: 'test@example.com', password: 'wrong' }
end
expect(response).to have_http_status(:too_many_requests)
# But general API still works
get '/api/articles', headers: auth_headers(user)
expect(response).to have_http_status(:ok)
end
end
end
Reference
Authentication Methods
| Method | Use Case | Security Level | Implementation Complexity |
|---|---|---|---|
| JWT | Stateless authentication, microservices | High with proper secret management | Medium |
| OAuth 2.0 | Third-party access, delegated authorization | High | High |
| API Keys | Service-to-service authentication | Medium | Low |
| Session Cookies | Traditional web apps, same-domain | Medium | Low |
| mTLS | High-security service communication | Very High | High |
Common HTTP Status Codes for Security
| Status Code | Meaning | When to Use |
|---|---|---|
| 401 Unauthorized | Missing or invalid authentication | Authentication failed or missing |
| 403 Forbidden | Authenticated but insufficient permissions | Authorization check failed |
| 429 Too Many Requests | Rate limit exceeded | Request throttling activated |
| 400 Bad Request | Invalid input parameters | Validation failed |
| 422 Unprocessable Entity | Semantically invalid request | Business logic validation failed |
Security Headers
| Header | Purpose | Example Value |
|---|---|---|
| Strict-Transport-Security | Enforce HTTPS | max-age=31536000; includeSubDomains |
| Content-Security-Policy | Restrict resource loading | default-src 'self' |
| X-Content-Type-Options | Prevent MIME sniffing | nosniff |
| X-Frame-Options | Prevent clickjacking | DENY |
| X-XSS-Protection | Enable XSS filtering | 1; mode=block |
Input Validation Checklist
| Input Type | Validation Requirements |
|---|---|
| Format validation, length limit, normalization | |
| Password | Minimum length, complexity requirements, no common passwords |
| Integer | Range validation, type coercion, overflow protection |
| String | Length limit, character whitelist, encoding validation |
| File Upload | Size limit, type validation, content verification, filename sanitization |
| URL | Protocol whitelist, domain validation, SSRF protection |
| JSON | Schema validation, depth limit, size limit |
Rate Limiting Strategies
| Strategy | Description | Use Case |
|---|---|---|
| Fixed Window | Count resets at fixed intervals | Simple implementations |
| Sliding Window | Rolling time window | More accurate throttling |
| Token Bucket | Allows bursts up to bucket size | APIs with variable load |
| Leaky Bucket | Smooth rate limiting | Prevent sudden spikes |
| Per-User | Different limits per user tier | Tiered service plans |
JWT Claims
| Claim | Type | Purpose |
|---|---|---|
| iss | Standard | Token issuer |
| sub | Standard | Subject identifier |
| aud | Standard | Intended audience |
| exp | Standard | Expiration time |
| iat | Standard | Issued at time |
| scope | Custom | Permission scopes |
| user_id | Custom | User identifier |
Common Vulnerabilities
| Vulnerability | Impact | Mitigation |
|---|---|---|
| SQL Injection | Database compromise | Parameterized queries, ORM usage |
| XSS | Session hijacking | Output encoding, CSP headers |
| CSRF | Unauthorized actions | Token verification, SameSite cookies |
| IDOR | Unauthorized data access | Object-level authorization checks |
| Mass Assignment | Privilege escalation | Strong parameters, field whitelisting |
| XXE | File disclosure, SSRF | Disable external entities, input validation |