Bleichenbacher Attack in Rails with Firestore
Bleichenbacher Attack in Rails with Firestore — how this specific combination creates or exposes the vulnerability
A Bleichenbacher attack is a chosen-ciphertext attack against asymmetric encryption, most commonly RSA PKCS#1 v1.5 padding. In a Rails application that uses Firestore as a persistence store for encrypted data or keys, the combination of Rails’ cryptographic handling and Firestore’s data storage behavior can expose a padding oracle when error responses differ based on decryption success. If your Rails service stores ciphertexts in Firestore (for example, in a users collection where encrypted_session_token is a field) and performs decryption in the app layer, an attacker can iteratively submit manipulated ciphertexts and observe timing or error differences to gradually decrypt data without the private key.
Consider a Rails controller that reads a Firestore document, decrypts a field, and uses it for authorization:
class TokensController < ApplicationController
def show
doc = FirestoreDb.collection("users").document(params[:id]).get
encrypted = doc.data["encrypted_session_token"]
# Raises OpenSSL::Cipher::CipherError on bad padding
decrypted = MyRsaUtil.decrypt_pkcs1(encrypted)
render plain: decrypted
rescue ActiveRecord::RecordNotFound
head 404
rescue OpenSSL::Cipher::CipherError => e
head 422
end
end
If MyRsaUtil.decrypt_pkcs1 uses OpenSSL::PKey::RSA with PKCS1_PADDING, a CipherError is raised for invalid padding. Distinguishing between a missing document (404) and bad padding (422) creates a server-side timing and status-code oracle. An attacker can adaptively modify ciphertext blocks and use the 422/404 or response-time differences to perform the classic Bleichenbacher iteration, eventually recovering the plaintext. Firestore itself does not introduce the vulnerability, but storing and retrieving encrypted blobs in a way that leaks decryption outcomes enables the attack chain.
Another scenario involves JWTs or encrypted API keys stored in Firestore with RSA-OAEP or RSA-PKCS1. If a Rails job processes queued tasks by pulling documents from a jobs collection and decrypts using a private key, inconsistent error handling or observable delays can be leveraged. For example:
class JobWorker
def process
item = FirestoreDb.collection("jobs").where("status", "==", "pending").limit(1).get
payload = MyRsaUtil.decrypt_pkcs1(item.data["payload"])
# process payload
end
end
If decryption errors are not uniformly handled (e.g., some raise, some log, some return generic errors), the channel becomes vulnerable. The attacker does not need direct network oracle access if the Rails app’s behavior differs based on padding correctness; Firestore simply acts as the storage medium for ciphertexts that are later decrypted in an observable way.
Specific to Firestore, beware of index-backed queries that return many documents; an attacker can batch ciphertexts and automate the oracle process across multiple documents. Also note that Firestore security rules do not mitigate application-layer decryption oracles—the rules govern document access, not the correctness of cryptographic operations.
Firestore-Specific Remediation in Rails — concrete code fixes
Remediation focuses on making decryption behavior constant-time and opaque, regardless of ciphertext validity, and avoiding leakage via status codes or timing. Do not rely on Firestore rules to protect encrypted payloads; treat Firestore as storage and handle cryptography defensively in Rails.
Use constant-time decryption and uniform error handling
Replace direct PKCS#1 decryption with a constant-time approach. For example, decrypt with OpenSSL::Cipher and then validate a known prefix or HMAC rather than trusting padding alone:
module MyRsaUtil
PRIVATE_KEY = OpenSSL::PKey::RSA.new(File.read("private_key.pem"))
PREFIX = "v1:secure:"
def self.decrypt_pkcs1_constant(ciphertext_b64)
ciphertext = Base64.strict_decode64(ciphertext_b64)
decrypted = PRIVATE_KEY.private_decrypt(ciphertext, OpenSSL::PKey::RSA::NO_PADDING)
# Remove leading zeros and split version/payload
clean = decrypted.sub(/^\x00+/, "")
# Constant-time check: compare fixed-length HMAC or known prefix
if clean.start_with?(PREFIX)
clean[PREFIX.length..]
else
# Return a deterministic dummy value to hide timing differences
PREFIX + "dummy_data_placeholder"
end
rescue OpenSSL::PKey::RSAError, ArgumentError => e
# Always return the same dummy payload on any decryption/parsing error
PREFIX + "dummy_data_placeholder"
end
end
In your controller, use the constant-time method and return a generic response:
class TokensController < ApplicationController
def show
doc = FirestoreDb.collection("users").document(params[:id]).get
if doc.nil?
head 404
else
decrypted = MyRsaUtil.decrypt_pkcs1_constant(doc.data["encrypted_session_token"])
# Validate decrypted content further before use
render plain: decrypted
end
rescue => e
# Generic error to avoid information leakage
head 500
end
end
This ensures that invalid ciphertexts do not produce distinct errors or timing channels. Note that private_decrypt with NO_PADDING is used only as an example; in practice, use a higher-level library that supports OAEP and constant-time verification, or use symmetric encryption for data at rest and store only symmetric keys encrypted with RSA.
Secure Firestore storage patterns
Store only opaque encrypted blobs in Firestore, and keep metadata separate. Use Rails credentials or a KMS-integrated key to encrypt data before it reaches Firestore, so the app never performs RSA decryption on untrusted input:
# Example: symmetric encryption with a Rails master key, store in Firestore
encrypted_payload = ActiveSupport::MessageEncryptor.new(Rails.application.credentials.secret_key_base).encrypt_and_sign(params[:token])
FirestoreDb.collection("users").document(user_id).update({ encrypted_token: encrypted_payload })
# Retrieval
doc = FirestoreDb.collection("users").document(user_id).get
token = ActiveSupport::MessageEncryptor.new(Rails.application.credentials.secret_key_base).decrypt_and_verify(doc.data["encrypted_token"])
render plain: token
If you must use RSA, perform decryption in a controlled environment (e.g., a separate service with strict input validation) and never expose padding errors to the Rails app. Also consider rotating keys and re-encrypting stored values to limit exposure. For workloads involving LLM endpoints or AI tooling, avoid sending sensitive decrypted material to external services; use local processing and keep Firestore fields minimal and non-sensitive where possible.
Operational practices
- Log decryption failures generically and monitor for unusual request patterns that may indicate probing.
- Use Firestore TTL policies to automatically expire old encrypted documents, reducing the window for ciphertext collection.
- Regularly rotate symmetric keys and audit access patterns via Firestore audit logs.
These changes reduce the risk of a Bleichenbacher-style oracle by eliminating distinguishable error paths and ensuring that Firestore does not leak information about decryption correctness through status codes or timing.