Overview
API security encompasses the strategies, protocols, and implementations that protect application programming interfaces from unauthorized access, data breaches, and malicious attacks. As APIs serve as the connective tissue between modern distributed systems, they represent critical attack surfaces that require multi-layered security controls.
The security challenge stems from APIs exposing application functionality and data to external consumers, often across network boundaries. Unlike traditional web applications where the user interface provides implicit control over interactions, APIs must enforce security programmatically at every endpoint. Each request carries potential risk - unauthorized access attempts, injection attacks, data exfiltration, or denial of service attacks.
API security operates across multiple dimensions. Authentication verifies caller identity. Authorization determines permitted actions. Encryption protects data in transit. Input validation prevents injection attacks. Rate limiting mitigates abuse. Logging and monitoring detect anomalies. Each dimension addresses specific threat vectors while contributing to defense in depth.
The shift toward microservices architectures and mobile applications has amplified API security importance. APIs now handle sensitive operations previously confined to server-side code - payment processing, personal data access, privilege escalation. A single compromised API endpoint can expose entire systems. The 2023 OWASP API Security Top 10 identifies broken object level authorization, broken authentication, and excessive data exposure as leading vulnerabilities.
Modern API security extends beyond perimeter defenses. Zero-trust architectures assume breach and verify every request. Token-based authentication eliminates shared secrets. Fine-grained authorization policies enforce least-privilege access. Encryption protects sensitive data even when systems are compromised. Security headers defend against cross-origin attacks and clickjacking.
# Basic secured API endpoint structure
class API::V1::UsersController < ApplicationController
before_action :authenticate_request
before_action :authorize_user, only: [:update, :destroy]
def show
@user = User.find(params[:id])
render json: @user.safe_attributes
rescue ActiveRecord::RecordNotFound
render json: { error: "Not found" }, status: :not_found
end
private
def authenticate_request
token = request.headers['Authorization']&.split(' ')&.last
@current_user = decode_token(token)
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
end
def authorize_user
user = User.find(params[:id])
render json: { error: "Forbidden" }, status: :forbidden unless can_access?(user)
end
end
Key Principles
Authentication establishes identity. APIs must verify that callers are who they claim to be before processing requests. Authentication mechanisms range from simple API keys to complex OAuth 2.0 flows. The selected approach depends on use case - server-to-server communication differs from user-facing mobile applications. Strong authentication prevents unauthorized access and provides audit trails for security incidents.
Token-based authentication has become the dominant pattern for stateless APIs. The server issues cryptographically signed tokens after successful credential verification. Clients include tokens in subsequent requests. The server validates token signatures without maintaining session state. JSON Web Tokens (JWT) encode claims about the authenticated entity along with expiration times and issuer information. Token-based approaches scale horizontally and work across distributed systems.
Authorization determines what authenticated callers can do. Authentication answers "who are you?" Authorization answers "what can you access?" Role-based access control (RBAC) assigns permissions to roles, then assigns roles to users. Attribute-based access control (ABAC) evaluates policies against user attributes, resource properties, and environmental conditions. Authorization policies enforce principle of least privilege - granting minimum necessary access.
Object-level authorization protects individual resources. API endpoints often accept object identifiers in parameters or request bodies. Broken object level authorization occurs when the server fails to verify that the authenticated user can access the specified object. Attackers modify identifiers to access resources belonging to other users. Every endpoint must validate that the current user owns or has permission for requested resources.
Encryption protects data confidentiality. Transport Layer Security (TLS) encrypts data in transit between clients and servers. All production APIs must use HTTPS. TLS prevents eavesdropping, tampering, and man-in-the-middle attacks. Certificate validation ensures clients connect to legitimate servers. Perfect forward secrecy generates unique session keys that cannot be compromised retroactively.
Sensitive data requires encryption at rest. API servers store credentials, tokens, and user data in databases. Encryption transforms plaintext into ciphertext using cryptographic algorithms. Only systems possessing decryption keys can access the original data. Field-level encryption protects specific attributes like credit card numbers or social security numbers.
Input validation sanitizes all data received from clients. Injection attacks exploit applications that trust user input. SQL injection inserts malicious database commands. Command injection executes arbitrary system commands. Cross-site scripting (XSS) injects malicious JavaScript. APIs must validate input types, formats, and ranges. Parameterized queries prevent SQL injection. Escaping output prevents XSS. Allowlists restrict input to known-safe values.
Rate limiting prevents abuse. APIs expose computational resources that attackers can exhaust through repeated requests. Rate limiting restricts the number of requests from a single source within a time window. Implementations track request counts per API key, IP address, or user identifier. Exceeded limits trigger HTTP 429 responses. Rate limiting mitigates denial of service attacks, brute force attempts, and data scraping.
Defense in depth applies multiple security layers. No single control provides complete protection. Authentication combined with authorization prevents both unauthorized access and privilege escalation. Encryption combined with input validation protects against eavesdropping and injection attacks. Monitoring combined with rate limiting detects and stops suspicious behavior. Overlapping controls create redundancy - if one layer fails, others maintain security.
# Comprehensive security middleware stack
class SecureAPI
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
# Enforce HTTPS in production
return redirect_to_https(request) unless secure_request?(request)
# Validate authentication token
token = extract_token(request)
user = authenticate_token(token)
return unauthorized_response unless user
# Check rate limits
return rate_limit_exceeded if rate_limited?(user)
# Add security headers
status, headers, body = @app.call(env)
headers.merge!(security_headers)
[status, headers, body]
end
private
def security_headers
{
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
'Content-Security-Policy' => "default-src 'self'"
}
end
end
Security Implications
Broken object level authorization represents the most critical API vulnerability. APIs accept object identifiers that specify which resources to return, modify, or delete. Without proper authorization checks, attackers substitute identifiers to access resources belonging to other users. The vulnerability stems from relying on client-provided identifiers without verifying ownership. Every endpoint that accepts resource identifiers must validate that the authenticated user has permission to access the specified resource.
Consider an API endpoint that returns user profiles: GET /api/users/:id. The naive implementation retrieves the requested user record and returns it. An attacker authenticates as user 100, then requests /api/users/200 to access another user's profile. Proper authorization compares the authenticated user identity against the requested resource owner before returning data.
Broken authentication compromises the foundation of API security. Common authentication failures include weak password policies, credential stuffing vulnerabilities, missing token expiration, and insecure token storage. APIs that accept credentials in URLs expose them in server logs and browser history. APIs that fail to invalidate tokens after password changes allow compromised tokens to remain valid indefinitely. Multi-factor authentication adds a second verification factor, significantly reducing account takeover risk.
Excessive data exposure occurs when APIs return more data than necessary. Developers often return entire database objects to clients, exposing fields like password hashes, internal identifiers, or sensitive attributes. The principle of minimal disclosure dictates that APIs return only required fields. Serialization layers should explicitly define exposed attributes rather than defaulting to all fields.
# Vulnerable - exposes all user attributes
def show
render json: User.find(params[:id])
end
# Secure - explicitly defines exposed attributes
def show
user = User.find(params[:id])
render json: {
id: user.id,
username: user.username,
email: user.email,
created_at: user.created_at
}
end
Lack of rate limiting enables denial of service and brute force attacks. APIs without request throttling allow attackers to overwhelm servers with traffic, exhaust database connections, or attempt credential guessing at scale. Rate limiting implementations track request counts per time window and reject excessive requests. Different endpoints may warrant different limits - authentication endpoints require stricter limits than read-only operations.
Mass assignment vulnerabilities occur when APIs automatically bind client-provided parameters to model attributes without filtering. An attacker includes unexpected parameters in requests to modify protected fields. The user registration endpoint accepts username and password but fails to reject an admin parameter. The attacker includes admin=true in the registration request to gain elevated privileges. Parameter filtering explicitly defines permitted attributes.
# Vulnerable to mass assignment
def create
user = User.create(params[:user])
render json: user
end
# Protected with strong parameters
def create
user = User.create(user_params)
render json: user
end
private
def user_params
params.require(:user).permit(:username, :email, :password)
end
Security misconfiguration manifests in numerous ways. Default credentials remain unchanged. Debug endpoints stay enabled in production. Verbose error messages reveal stack traces and database schema. CORS policies allow all origins. Security headers are missing. The attack surface expands with each misconfiguration. Configuration management must explicitly set secure defaults and validate settings during deployment.
Injection attacks exploit insufficient input validation. SQL injection embeds malicious SQL in user input that gets concatenated into database queries. Command injection includes shell metacharacters in parameters that get passed to system commands. NoSQL injection manipulates queries in document databases. LDAP injection targets directory services. XML External Entity (XXE) injection exploits XML parsers to read files or cause denial of service.
Insufficient logging and monitoring blinds defenders to attacks in progress. APIs should log authentication failures, authorization violations, input validation failures, and security-relevant events. Logs must include sufficient context - timestamp, user identifier, IP address, requested resource, action attempted. Automated monitoring detects anomalous patterns like spike in failed authentication or unusual geographic access patterns. Alerting notifies security teams of potential incidents.
Insecure deserialization creates remote code execution risks. APIs that deserialize untrusted data without validation allow attackers to inject malicious objects. During deserialization, the application instantiates objects and may execute code defined in object methods. Attackers craft serialized payloads that instantiate dangerous classes or trigger side effects during deserialization. Avoid deserializing untrusted data or use safe serialization formats like JSON.
Cross-Origin Resource Sharing (CORS) misconfigurations expose APIs to cross-origin attacks. Browsers prevent JavaScript from making requests to different origins unless the server explicitly allows it via CORS headers. Setting Access-Control-Allow-Origin: * permits all origins, enabling malicious sites to make authenticated requests from victim browsers. CORS policies should explicitly list allowed origins and validate the Origin header.
Ruby Implementation
Rails provides built-in security features for API development. The ActionController::API base class offers a lightweight controller stack optimized for API responses. Rails automatically protects against CSRF attacks, SQL injection through ActiveRecord, and mass assignment through strong parameters. The security foundation requires explicit opt-in for additional protections.
Authentication in Rails APIs typically uses token-based approaches. The has_secure_password method provides bcrypt password hashing. Developers implement custom authentication controllers that verify credentials and issue tokens. The JWT gem generates JSON Web Tokens containing encrypted claims. Controllers include authentication checks in before_action callbacks.
# Authentication controller
class API::AuthenticationController < ApplicationController
skip_before_action :authenticate_request
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = generate_token(user)
render json: { token: token, user: user.as_json(only: [:id, :email]) }
else
render json: { error: "Invalid credentials" }, status: :unauthorized
end
end
private
def generate_token(user)
payload = {
user_id: user.id,
exp: 24.hours.from_now.to_i
}
JWT.encode(payload, Rails.application.credentials.secret_key_base)
end
end
# Application controller with authentication
class ApplicationController < ActionController::API
before_action :authenticate_request
private
def authenticate_request
header = request.headers['Authorization']
token = header.split(' ').last if header
begin
decoded = JWT.decode(token, Rails.application.credentials.secret_key_base).first
@current_user = User.find(decoded['user_id'])
rescue JWT::ExpiredSignature
render json: { error: "Token expired" }, status: :unauthorized
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
render json: { error: "Invalid token" }, status: :unauthorized
end
end
attr_reader :current_user
end
The Pundit gem provides authorization policies. Each model has a corresponding policy class defining who can perform which actions. Controllers call authorize methods before operations. Policies encapsulate authorization logic in dedicated classes rather than scattering conditional checks across controllers.
# User policy
class UserPolicy < ApplicationPolicy
def show?
user == record || user.admin?
end
def update?
user == record
end
def destroy?
user.admin?
end
end
# Controller using Pundit
class API::UsersController < ApplicationController
def show
@user = User.find(params[:id])
authorize @user
render json: @user
end
def update
@user = User.find(params[:id])
authorize @user
if @user.update(user_params)
render json: @user
else
render json: { errors: @user.errors }, status: :unprocessable_entity
end
end
end
Rate limiting protection uses Rack::Attack middleware. The gem provides a DSL for defining throttle rules, blocklists, and allowlists. Throttles limit requests per time period. Blocklists reject traffic from specific IPs or patterns. Allowlists exempt trusted sources from restrictions.
# config/initializers/rack_attack.rb
class Rack::Attack
# Throttle authentication attempts
throttle('auth/ip', limit: 5, period: 60) do |req|
req.ip if req.path == '/api/auth' && req.post?
end
# Throttle API requests per authenticated user
throttle('api/user', limit: 100, period: 60) do |req|
req.env['current_user']&.id if req.path.start_with?('/api')
end
# Block IPs making excessive requests
blocklist('block bad actors') do |req|
Blocklist.blocked?(req.ip)
end
# Custom response for throttled requests
self.throttled_response = 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
OAuth 2.0 implementation uses the Doorkeeper gem. Doorkeeper provides authorization server functionality, generating access tokens and refresh tokens. The gem integrates with existing authentication systems and supports multiple grant types - authorization code, client credentials, resource owner password credentials.
# Doorkeeper configuration
Doorkeeper.configure do
orm :active_record
resource_owner_authenticator do
User.find_by(id: session[:user_id]) || redirect_to(login_url)
end
# Access token expiration
access_token_expires_in 2.hours
# Grant flows to enable
grant_flows %w[authorization_code client_credentials]
# Token authentication methods
access_token_methods :from_bearer_authorization
end
# Protected API endpoint
class API::ResourcesController < ApplicationController
before_action -> { doorkeeper_authorize! :read }
def index
@resources = Resource.where(user: current_resource_owner)
render json: @resources
end
private
def current_resource_owner
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end
end
Secure password storage requires proper hashing. The bcrypt gem implements the bcrypt algorithm, designed specifically for password hashing. Bcrypt includes automatic salt generation and configurable work factors. Never store passwords in plaintext or use weak hashing algorithms like MD5 or SHA1.
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
validates :password, length: { minimum: 12 }, if: :password_required?
private
def password_required?
password_digest.blank? || password.present?
end
end
API versioning maintains backward compatibility while introducing security improvements. URL-based versioning embeds version numbers in paths. Header-based versioning uses custom headers or Accept headers. Deprecated versions can enforce stricter security requirements or reject requests entirely.
# Version-based routing
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users
end
namespace :v2 do
resources :users
end
end
end
# Version-specific security requirements
module API
module V1
class ApplicationController < ActionController::API
before_action :deprecation_warning
private
def deprecation_warning
response.headers['Warning'] = '299 - "API v1 is deprecated. Migrate to v2"'
end
end
end
end
Implementation Approaches
API key authentication provides simple authentication for server-to-server communication. The API provider generates unique keys for each client. Clients include keys in request headers or query parameters. The server validates keys against stored values. API keys work well for identifying applications but cannot represent individual users. Key rotation policies mitigate compromise risk. Never embed API keys in client-side code or public repositories.
# API key generation and storage
class APIKey < ApplicationRecord
belongs_to :user
before_create :generate_key
private
def generate_key
self.key = SecureRandom.hex(32)
self.key_hash = Digest::SHA256.hexdigest(key)
end
def self.authenticate(key)
key_hash = Digest::SHA256.hexdigest(key)
find_by(key_hash: key_hash)
end
end
# Controller authentication
def authenticate_api_key
key = request.headers['X-API-Key']
@api_key = APIKey.authenticate(key)
render json: { error: "Invalid API key" }, status: :unauthorized unless @api_key
end
OAuth 2.0 handles delegated authorization. Users grant third-party applications limited access to resources without sharing passwords. The authorization server issues access tokens after user consent. Resource servers validate tokens before serving requests. OAuth 2.0 defines multiple flows for different scenarios - authorization code for web applications, implicit for single-page apps, client credentials for service accounts.
The authorization code flow provides the strongest security. The client redirects users to the authorization server. After authentication and consent, the authorization server redirects back with an authorization code. The client exchanges the code for an access token through a direct server-to-server request. This prevents token exposure in URLs or browser history.
JWT tokens encode claims in a JSON structure. The token contains three parts: header, payload, and signature. The header specifies the signing algorithm. The payload contains claims like user ID and expiration time. The signature ensures integrity - tampering invalidates the token. JWTs enable stateless authentication since servers validate tokens cryptographically without database lookups.
# JWT encoding with custom claims
def encode_token(user, permissions = [])
payload = {
user_id: user.id,
email: user.email,
permissions: permissions,
iat: Time.now.to_i,
exp: 24.hours.from_now.to_i
}
JWT.encode(payload, jwt_secret, 'HS256')
end
# JWT decoding with verification
def decode_token(token)
JWT.decode(
token,
jwt_secret,
true,
{
algorithm: 'HS256',
verify_expiration: true,
verify_iat: true
}
).first
rescue JWT::DecodeError => e
nil
end
Session-based authentication maintains state on the server. After successful login, the server creates a session and returns a session identifier to the client. Subsequent requests include the session ID. The server looks up session data to authenticate requests. Sessions work well for traditional web applications but scale poorly for distributed systems. Session stores can use databases, Redis, or memory stores.
Mutual TLS authentication verifies both client and server identities using certificates. The server requires clients to present valid certificates signed by a trusted authority. Both parties verify certificates during the TLS handshake. Mutual TLS provides strong authentication for service-to-service communication. Certificate management requires infrastructure for issuance, renewal, and revocation.
Token refresh mechanisms extend authentication without requiring reauthentication. Short-lived access tokens minimize risk if compromised. Long-lived refresh tokens enable obtaining new access tokens. When an access token expires, the client presents the refresh token to receive a new access token. The authorization server can revoke refresh tokens, terminating access immediately.
# Token refresh implementation
class API::TokensController < ApplicationController
skip_before_action :authenticate_request, only: [:refresh]
def refresh
refresh_token = params[:refresh_token]
stored_token = RefreshToken.find_by(token_hash: hash_token(refresh_token))
if stored_token&.valid?
new_access_token = generate_access_token(stored_token.user)
render json: { access_token: new_access_token }
else
render json: { error: "Invalid refresh token" }, status: :unauthorized
end
end
private
def hash_token(token)
Digest::SHA256.hexdigest(token)
end
end
Common Pitfalls
Hardcoded credentials in source code expose secrets to anyone with repository access. Developers often commit API keys, database passwords, or signing secrets directly in code. Version control systems preserve this information in commit history even after removal. Use environment variables or secret management services for sensitive values. Scan repositories for accidentally committed secrets.
Weak token generation creates predictable values attackers can guess. Sequential integers, timestamps, or simple random numbers fail to provide sufficient entropy. Use cryptographically secure random number generators. Ruby's SecureRandom module generates tokens with adequate randomness. Tokens should have sufficient length - 32 bytes minimum for API keys.
# Weak token generation - predictable
token = rand(1000000).to_s # Bad
# Secure token generation
token = SecureRandom.hex(32) # Good
token = SecureRandom.urlsafe_base64(32) # Also good
Missing token expiration allows compromised tokens to remain valid indefinitely. Tokens should include expiration times and servers must validate them. Short expiration windows limit damage from token compromise. Refresh tokens enable extending sessions without keeping access tokens valid for extended periods.
Exposing detailed error messages reveals system internals to attackers. Stack traces expose code structure, file paths, and dependencies. Database errors reveal schema information. Verbose messages help attackers map the attack surface. Return generic error messages to clients while logging detailed information server-side.
# Detailed error exposure - vulnerable
rescue ActiveRecord::RecordNotFound => e
render json: { error: e.message, backtrace: e.backtrace }, status: :not_found
end
# Generic error messages - secure
rescue ActiveRecord::RecordNotFound
render json: { error: "Resource not found" }, status: :not_found
# Log detailed error server-side
Rails.logger.error("Record not found: #{e.message}")
end
Insufficient rate limiting enables brute force attacks and API abuse. Authentication endpoints without rate limits allow unlimited password guessing attempts. Resource-intensive operations without limits exhaust server capacity. Apply different rate limits based on endpoint sensitivity and resource consumption.
Trusting client-provided data without validation creates injection vulnerabilities. Assume all input is malicious until proven otherwise. Validate data types, formats, and ranges. Use parameterized queries for database operations. Escape output when rendering user-provided content. Apply allowlists for known-safe values rather than blocklists for dangerous patterns.
Using GET requests for state-changing operations violates HTTP semantics and creates security issues. GET requests appear in server logs, browser history, and may be cached by intermediaries. Credentials or sensitive data in URLs gets exposed in multiple places. Use POST, PUT, PATCH, or DELETE for operations that modify state.
Failing to validate Content-Type headers enables type confusion attacks. Attackers send JSON payloads with Content-Type: text/html to bypass JSON parsing protections. Validate that Content-Type matches expected values before processing request bodies. Reject requests with mismatched content types.
Missing security headers leave applications vulnerable to browser-based attacks. Strict-Transport-Security enforces HTTPS. X-Frame-Options prevents clickjacking. Content-Security-Policy restricts resource loading. X-Content-Type-Options prevents MIME sniffing. Set security headers on all API responses.
# Comprehensive security headers
class API::ApplicationController < ActionController::API
after_action :set_security_headers
private
def set_security_headers
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
response.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'
end
end
Inadequate CORS configuration creates cross-origin security issues. Setting Access-Control-Allow-Origin: * permits all origins including malicious sites. Validate Origin headers and explicitly list allowed origins. Never reflect the Origin header in Access-Control-Allow-Origin without validation.
Logging sensitive data creates compliance and security issues. Passwords, tokens, credit card numbers, and personally identifiable information should never appear in logs. Filter sensitive parameters before logging. Encrypt logs at rest. Implement log retention policies.
Testing Approaches
Security testing requires different strategies than functional testing. Unit tests verify authorization logic in isolation. Integration tests confirm security controls work across components. Penetration testing simulates real attacks. Automated scanning identifies common vulnerabilities.
Authorization testing ensures policies enforce access controls correctly. Test that authenticated users can only access permitted resources. Verify that unauthorized access attempts fail appropriately. Test boundary conditions - resources the user owns versus resources belonging to others.
# RSpec authorization tests
RSpec.describe API::UsersController, type: :request do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:admin) { create(:user, :admin) }
describe 'GET /api/users/:id' do
context 'when accessing own profile' do
it 'returns the user profile' do
get "/api/users/#{user.id}", headers: auth_headers(user)
expect(response).to have_http_status(:ok)
end
end
context 'when accessing other user profile' do
it 'returns forbidden' do
get "/api/users/#{other_user.id}", headers: auth_headers(user)
expect(response).to have_http_status(:forbidden)
end
end
context 'when admin accessing any profile' do
it 'returns the user profile' do
get "/api/users/#{user.id}", headers: auth_headers(admin)
expect(response).to have_http_status(:ok)
end
end
end
end
Authentication testing validates credential verification and token handling. Test successful authentication with valid credentials. Test authentication failures with invalid credentials. Verify token expiration handling. Test token refresh mechanisms. Confirm that token revocation prevents access.
# Authentication test suite
RSpec.describe 'API Authentication', type: :request do
let(:user) { create(:user, password: 'SecurePassword123!') }
describe 'POST /api/auth' do
context 'with valid credentials' do
it 'returns a token' do
post '/api/auth', params: {
email: user.email,
password: 'SecurePassword123!'
}
expect(response).to have_http_status(:ok)
expect(json_response['token']).to be_present
end
end
context 'with invalid password' do
it 'returns unauthorized' do
post '/api/auth', params: {
email: user.email,
password: 'wrong'
}
expect(response).to have_http_status(:unauthorized)
expect(json_response['token']).to be_nil
end
end
context 'with expired token' do
it 'rejects the request' do
token = generate_token(user, exp: 1.hour.ago)
get '/api/protected', headers: { 'Authorization' => "Bearer #{token}" }
expect(response).to have_http_status(:unauthorized)
end
end
end
end
Input validation testing confirms that APIs reject malicious input. Test SQL injection attempts in query parameters. Test command injection in file operations. Test XSS payloads in text fields. Test oversized inputs that could cause denial of service. Test unexpected data types.
Rate limiting testing verifies throttling mechanisms function correctly. Make sequential requests up to the limit and confirm they succeed. Make one additional request and verify it receives a 429 status. Wait for the time window to reset and confirm requests succeed again. Test rate limiting per user, per IP, and per API key.
# Rate limiting test
RSpec.describe 'Rate Limiting', type: :request do
let(:user) { create(:user) }
it 'limits requests per time window' do
headers = auth_headers(user)
# Make requests up to limit
5.times do
get '/api/resources', headers: headers
expect(response).to have_http_status(:ok)
end
# Sixth request should be rate limited
get '/api/resources', headers: headers
expect(response).to have_http_status(:too_many_requests)
expect(response.headers['Retry-After']).to be_present
end
end
Security scanning identifies vulnerabilities through automated analysis. Tools like Brakeman scan Rails applications for security issues. OWASP ZAP performs dynamic analysis by making HTTP requests. Dependency scanners identify vulnerable gems. Integrate security scanning into continuous integration pipelines.
Penetration testing involves manual security assessment by skilled testers. Testers attempt to exploit vulnerabilities through creative attack techniques. Penetration testing discovers logic flaws and complex vulnerabilities that automated tools miss. Conduct penetration testing before major releases and periodically for production systems.
Fuzzing tests application behavior with random or malformed input. Fuzzers generate thousands of input variations to trigger unexpected behavior. Effective for discovering input validation gaps, crashes, and edge cases. Fuzzing tools can target API endpoints, file parsers, and protocol handlers.
Reference
Authentication Methods Comparison
| Method | Use Case | Security Level | Complexity | Stateless |
|---|---|---|---|---|
| API Keys | Server-to-server | Medium | Low | Yes |
| OAuth 2.0 | Third-party delegation | High | High | Yes |
| JWT | Distributed systems | High | Medium | Yes |
| Session-based | Traditional web apps | Medium | Low | No |
| Mutual TLS | Service mesh | Very High | High | Yes |
HTTP Security Headers
| Header | Purpose | Example Value |
|---|---|---|
| Strict-Transport-Security | Enforce HTTPS | max-age=31536000; includeSubDomains |
| X-Frame-Options | Prevent clickjacking | DENY |
| X-Content-Type-Options | Prevent MIME sniffing | nosniff |
| Content-Security-Policy | Restrict resource loading | default-src 'self' |
| X-XSS-Protection | Enable XSS filter | 1; mode=block |
| Referrer-Policy | Control referrer information | strict-origin-when-cross-origin |
Common HTTP Status Codes for Security
| Code | Name | When to Use |
|---|---|---|
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but lacks permission |
| 429 | Too Many Requests | Rate limit exceeded |
| 400 | Bad Request | Invalid input format |
| 422 | Unprocessable Entity | Valid format but fails validation |
OWASP API Security Top 10
| Rank | Vulnerability | Description |
|---|---|---|
| API1 | Broken Object Level Authorization | Accessing objects without ownership verification |
| API2 | Broken Authentication | Weak credential handling or session management |
| API3 | Broken Object Property Level Authorization | Exposing or allowing modification of sensitive properties |
| API4 | Unrestricted Resource Consumption | Missing rate limiting or resource quotas |
| API5 | Broken Function Level Authorization | Accessing administrative functions without proper authorization |
| API6 | Unrestricted Access to Sensitive Business Flows | Missing protection for critical business operations |
| API7 | Server Side Request Forgery | API making requests to attacker-controlled URLs |
| API8 | Security Misconfiguration | Insecure defaults or missing security hardening |
| API9 | Improper Inventory Management | Undocumented or deprecated endpoints |
| API10 | Unsafe Consumption of APIs | Trusting data from third-party APIs without validation |
Token Generation Best Practices
| Aspect | Recommendation | Example |
|---|---|---|
| Length | Minimum 32 bytes | SecureRandom.hex(32) |
| Encoding | URL-safe base64 | SecureRandom.urlsafe_base64(32) |
| Expiration | 1-24 hours for access tokens | exp: 1.hour.from_now |
| Storage | Hashed in database | SHA256 hash of token |
| Transmission | HTTPS only | Never in URLs |
Input Validation Checklist
| Validation Type | Check | Implementation |
|---|---|---|
| Type validation | Correct data type | Strong parameters |
| Format validation | Matches expected pattern | Regex or format validators |
| Range validation | Within acceptable bounds | Numericality validators |
| Length validation | Appropriate size | Length validators |
| Allowlist validation | Known safe values | Inclusion validators |
| Sanitization | Remove dangerous characters | HTML escaping, SQL parameterization |
Rate Limiting Strategies
| Strategy | Tracks By | Use Case |
|---|---|---|
| Per IP | Client IP address | Anonymous endpoints |
| Per User | Authenticated user ID | User-specific actions |
| Per API Key | API key identifier | Partner integrations |
| Per Endpoint | Request path | Protecting expensive operations |
| Sliding Window | Rolling time period | Smooth traffic distribution |
| Token Bucket | Available tokens | Burst allowance |
Security Testing Types
| Test Type | Focus | Frequency |
|---|---|---|
| Unit tests | Authorization logic | Every commit |
| Integration tests | End-to-end security flows | Every build |
| Static analysis | Code vulnerabilities | Every commit |
| Dependency scanning | Vulnerable libraries | Daily |
| Dynamic scanning | Runtime vulnerabilities | Weekly |
| Penetration testing | Manual exploitation | Quarterly |
Essential Ruby Security Gems
| Gem | Purpose | Configuration |
|---|---|---|
| bcrypt | Password hashing | has_secure_password |
| jwt | JSON Web Tokens | JWT.encode/decode |
| doorkeeper | OAuth 2.0 provider | Doorkeeper.configure |
| pundit | Authorization policies | Policy classes |
| rack-attack | Rate limiting | Throttle definitions |
| brakeman | Security scanning | Static analysis |
| devise | Authentication | Full auth system |
| secure_headers | Security header management | Header configuration |