Excessive Data Exposure in Grape
How Excessive Data Exposure Manifests in Grape
Excessive Data Exposure in Grape APIs occurs when endpoints return more data than necessary, exposing sensitive information to attackers. This vulnerability is particularly prevalent in Grape due to its DSL-based approach and the way developers structure their API responses.
The most common manifestation appears in route definitions where developers use present or expose without proper attribute filtering. Consider this typical Grape pattern:
class UsersAPI < Grape::API
format :json
get '/users/:id' do
user = User.find(params[:id])
present user, with: Entities::User
end
end
The issue arises when the Entities::User entity exposes all attributes by default:
class Entities::User < Grape::Entity
expose :id
expose :name
expose :email
expose :phone
expose :address
expose :created_at
expose :updated_at
expose :password_digest
expose :ssn
expose :credit_card
end
Here, fields like password_digest, ssn, and credit_card should never be exposed to API consumers, yet they're readily available if an attacker discovers the endpoint.
Another Grape-specific pattern involves nested entities with recursive exposure. When entities reference each other without proper depth control, attackers can exploit this to extract more data than intended:
class Entities::User < Grape::Entity
expose :id
expose :name
expose :email
expose :orders, using: Entities::Order
end
class Entities::Order < Grape::Entity
expose :id
expose :total
expose :user, using: Entities::User # RECURSIVE EXPOSURE
end
This creates a potential for infinite recursion or excessive data payloads that can be exploited for information gathering.
Grape's flexible parameter handling also contributes to this issue. Developers often use params[:all] or similar broad parameter access patterns:
get '/users' do
users = User.where(params[:all])
present users, with: Entities::User
end
This allows attackers to manipulate query parameters to access data they shouldn't see, such as filtering by role=admin to find administrative users.
Version-specific exposure is another Grape vulnerability. Developers might expose sensitive fields in newer API versions while forgetting to restrict them:
version 'v2', using: :header, vendor: 'myapp' do
get '/users/:id' do
user = User.find(params[:id])
present user, with: Entities::UserV2 # EXPOSES SENSITIVE FIELDS
end
end
Without proper access controls, version v2 might expose fields that were restricted in v1, creating a security regression.
Grape-Specific Detection
Detecting Excessive Data Exposure in Grape APIs requires both static code analysis and runtime scanning. middleBrick's approach combines these methods to identify vulnerabilities specific to Grape's architecture.
For static detection, middleBrick analyzes Grape entity definitions and route configurations. It looks for patterns like:
# Suspicious entity definition
class Entities::User < Grape::Entity
expose :id
expose :name
expose :email
expose :password_digest # FLAGGED: Sensitive field exposure
expose :ssn # FLAGGED: PII exposure
end
The scanner identifies entities that expose fields containing keywords like password, secret, token, key, ssn, credit, ssn, dob, address, and similar sensitive terms.
Runtime detection involves scanning actual API responses to verify what data is being returned. middleBrick's black-box scanning tests endpoints and analyzes the JSON structure:
{
"id": 123,
"name": "John Doe",
"email": "[email protected]",
"password_digest": "$2a$12$Kvi...",
"ssn": "123-45-6789",
"credit_card": "4111111111111111"
}
The scanner flags responses containing sensitive data fields, even if they're not documented in the OpenAPI spec.
middleBrick also tests for parameter manipulation vulnerabilities specific to Grape:
# Test for parameter-based data exposure
curl -X GET "https://api.example.com/users?role=admin&status=active"
If the endpoint returns administrative users or sensitive data based on these parameters, it indicates a vulnerability.
For entity recursion detection, middleBrick analyzes the entity graph to identify circular references and excessive nesting depth:
# Detected recursive exposure
class Entities::User < Grape::Entity
expose :id
expose :name
expose :orders, using: Entities::Order
end
class Entities::Order < Grape::Entity
expose :id
expose :total
expose :user, using: Entities::User # RECURSIVE
end
The scanner calculates the potential response size and flags configurations that could lead to excessive data exposure or denial-of-service through large responses.
Version-specific exposure testing verifies that newer API versions don't inadvertently expose more data than intended:
# Test different API versions
curl -H "Accept: application/vnd.myapp-v2+json" \
https://api.example.com/users/123
middleBrick compares responses across versions to identify data exposure regressions.
Grape-Specific Remediation
Remediating Excessive Data Exposure in Grape requires a multi-layered approach using Grape's built-in features and Ruby best practices. The most effective strategy combines attribute whitelisting, role-based exposure, and careful entity design.
Start with strict attribute whitelisting in your entities. Never expose all attributes by default:
class Entities::User < Grape::Entity
# ONLY expose what's absolutely necessary
expose :id, documentation: { type: 'Integer', desc: 'User ID' }
expose :name, documentation: { type: 'String', desc: 'Full name' }
# CONDITIONAL exposure based on context
expose :email, if: { type: :public } do |user, opts|
user.email if opts[:current_user]&.admin?
end
# BLOCK exposure for computed fields
expose :full_address, if: { type: :admin } do |user, opts|
next unless opts[:current_user]&.admin?
[user.address, user.city, user.state, user.zip].compact.join(', ')
end
# EXCLUDE sensitive fields entirely
# No exposure for: password_digest, ssn, credit_card, etc.
end
Use Grape's conditional exposure (if: option) to control data visibility based on user roles and request context. This prevents unauthorized access to sensitive information.
Implement proper parameter filtering to prevent data leakage through query manipulation:
class UsersAPI < Grape::API
params do
optional :role, type: String, values: ['user', 'admin']
optional :status, type: String, values: ['active', 'inactive']
# DO NOT allow arbitrary field filtering
end
get '/users' do
# WHITELIST allowed filters
allowed_filters = [:role, :status, :created_after]
filtered_params = declared(params, include_missing: false)
users = User.all
users = users.where(filtered_params.slice(*allowed_filters))
present users, with: Entities::User
end
end
This approach prevents attackers from using parameters like password=1 or ssn=1 to manipulate queries.
For nested entities, implement depth control and proper exposure limits:
class Entities::User < Grape::Entity
expose :id
expose :name
expose :email
# LIMIT nested exposure
expose :orders, using: Entities::Order, if: { type: :detailed } do |user, opts|
next [] unless opts[:include_orders]
user.orders.limit(10) # LIMIT result set
end
end
class Entities::Order < Grape::Entity
expose :id
expose :total
expose :created_at
# DO NOT expose user reference to prevent recursion
# expose :user, using: Entities::User # REMOVE this line
end
Break recursive relationships and use pagination or limits to control response size.
Implement version-specific entities with proper field restrictions:
class Entities::UserV1 < Grape::Entity
expose :id
expose :name
expose :email
end
class Entities::UserV2 < Grape::Entity
expose :id
expose :name
expose :email
# NO additional sensitive fields exposed
end
Ensure newer versions don't accidentally expose more data than previous versions.
Use Grape's documentation features to explicitly mark sensitive fields and their exposure conditions:
class Entities::User < Grape::Entity
expose :id, documentation: { type: 'Integer', desc: 'User ID' }
expose :name, documentation: { type: 'String', desc: 'Full name' }
expose :email, if: { type: :admin }, \
documentation: { type: 'String', desc: 'Email address (admin only)' }
end
This makes the exposure rules explicit and helps prevent accidental data leakage.
Finally, implement comprehensive testing for data exposure:
require 'rspec'
describe Entities::User do
let(:user) { create(:user, email: '[email protected]') }
it 'does not expose sensitive fields' do
result = Entities::User.represent(user, type: :public)
json = JSON.parse(result.to_json)
expect(json.keys).to contain_exactly('id', 'name')
expect(json).not_to have_key('email')
expect(json).not_to have_key('password_digest')
expect(json).not_to have_key('ssn')
end
it 'exposes email only to admins' do
result = Entities::User.represent(user, type: :admin)
json = JSON.parse(result.to_json)
expect(json.keys).to include('email')
end
end
Regular security scanning with middleBrick helps catch any regressions in data exposure controls.
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |