Api Key Exposure in Grape with Postgresql
Api Key Exposure in Grape with Postgresql — how this specific combination creates or exposes the vulnerability
Grape is a REST-like API micro-framework for Ruby that sits on top of Rack. When endpoints return sensitive data such as API keys stored in a Postgresql database, improper handling can lead to API key exposure. This typically occurs when database records are serialized directly into JSON responses without filtering sensitive fields, when logs capture parameters or payloads containing keys, or when error messages reveal stack traces or query details that reference key material.
In a Grape API using Postgresql as the data store, a common pattern is to have an API key column on a table (for example, an api_keys table or a users table with a column like api_key_hash or api_key). If a route like GET /v1/api_key retrieves the record and renders it via present or to_json without explicitly excluding the key, the raw key can be returned to the client. Similarly, logging the incoming params in a before filter can write keys to logs, and unhandled exceptions may surface SQL snippets or model attributes that include key values if the error handler is not careful.
Another exposure vector arises from introspection or debugging endpoints that query Postgresql system catalogs or use dynamic models without scoping. For instance, an endpoint that enumerates tables or columns and echoes user-supplied input into a query can inadvertently surface metadata or rows that contain stored keys. Insufficient authorization checks (BOLA/IDOR) can allow one client to request another client’s key-bearing records, and weak parameter filtering can cause mass assignment of key fields during creation or updates.
The risk is compounded when Grape resources do not consistently apply strong hashing (e.g., bcrypt or Argon2) before storing keys in Postgresql. Plaintext or weakly protected keys in the database increase the impact of a read exposure. Even if keys are hashed, returning database rows that include the hash without redaction can aid offline attacks if the hashes are cracked.
To detect this class of issue, middleBrick runs checks aligned with the OWASP API Top 10 and maps findings to relevant standards. For Grape + Postgresql services, look for routes that fetch sensitive columns, missing filters in serialization, and overly verbose error messages that may leak query context or key material.
Postgresql-Specific Remediation in Grape — concrete code fixes
Remediation focuses on preventing sensitive API key material from appearing in responses, logs, or errors when using Postgresql-backed Grape services. Apply these patterns consistently across resource files and shared helpers.
1) Exclude sensitive fields during serialization
Use except when converting ActiveRecord or plain objects to JSON, and avoid automatic rendering of attributes. In Grape, prefer explicit present with a selected subset of fields.
# app/resources/api_key_resource.rb
class ApiKeyResource < Grape::Entity
expose :id, :created_at, :owner_id
# Do NOT expose :key or :api_key
end
# In your resource block
resource :api_key do
desc 'Retrieve non-sensitive key metadata only'
get do
key_record = KeyModel.find(params[:id])
present key_record, with: ApiKeyResource
end
end
2) Strong storage hashing and verification
Store only a hash of the key in Postgresql. When verifying, compare hashes using a constant-time method. Never store or return the raw key after creation.
# Migration
create_table :api_keys do |t|
t.references :owner, null: false, foreign_key: true
t.string :key_hash, null: false
t.string :key_id, null: false, unique: true
t.timestamps
end
# Model
class KeyModel < ActiveRecord::Base
has_secure_password # or use bcrypt/lockbox directly
def self.create_key_pair
raw = SecureRandom.urlsafe_base64(32)
create!(key_id: "key_#{SecureRandom.uuid}", key_hash: BCrypt::Password.create(raw))
raw # return once to caller, then discard
end
def self.verify_key(id, candidate)
rec = find_by(key_id: id)
return false unless rec
BCrypt::Password.new(rec.key_hash) == candidate
end
end
3) Parameter filtering and logging hygiene
Ensure Grape filters sensitive params globally and that logs do not capture keys. Combine Rails filter_parameters with explicit scrubbing in before filters.
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [:api_key, :key, :secret]
# app/api/base_api.rb
class BaseApi < Grape::API
before { header['Access-Control-Allow-Origin'] = '*' }
before { env['api.request.query.params'] = filter_sensitive(env['api.request.query.params']) }
helpers do
def filter_sensitive(params)
return params unless params.is_a?(Hash)
params.except(:api_key, :key, :secret).tap do |filtered|
filtered.each do |k, v|
filtered[k] = '[FILTERED]' if %i[key api_key secret].include?(k)
end
end
end
end
end
4) Safe error handling and introspection controls
Avoid exposing stack traces or query details. Rescue exceptions generically and do not echo back parameters that may contain keys. Disable dynamic model introspection in Grape endpoints.
# app/api/base_api.rb
rescue_from :all do |e|
# Log the exception ID for internal tracking without exposing details
error!({ error: 'Internal server error' }, 500)
end
# Disable automatic params echoing
configure do
formatter :json, { error: 'Request failed' }
end
5) Authorization and scoping
Always scope queries to the requester to prevent BOLA/IDOR. Use policy objects or scopes that ensure a user can only access their own key records.
# app/policies/key_policy.rb
class KeyPolicy
def initialize(user, record)
@user = user
@record = record
end
def show?
record.owner_id == user.id
end
end
# In resource
resource :api_key do
desc 'Get own key metadata'
get ':id' do
key = KeyPolicy.new(current_user, KeyModel.find(params[:id]))
error!('Forbidden', 403) unless key.show?
present key, with: ApiKeyResource
end
end