Insecure Deserialization in Chi with Firestore
Insecure Deserialization in Chi with Firestore — how this specific combination creates or exposes the vulnerability
Insecure deserialization occurs when an application accepts untrusted data and reconstructs objects from it without sufficient validation. In Chi, a common pattern is to bind incoming JSON directly into Elixir structs or maps for domain modeling, then store or retrieve those structures in Firestore. Firestore does not enforce a schema on the server, so serialized terms (maps, structs, tagged tuples) can be written and later read back. If the application reconstructs objects using functions like apply/2, Code.eval_string/1, or libraries that perform deep merging of maps into structs without verifying types, an attacker can supply crafted payloads that cause arbitrary code execution or logic manipulation when the data is deserialized.
Chi routes typically pattern-match on connection and assign parameters to a changeset or directly to a map. Consider accepting a JSON body into a function that later stores a serialized representation in Firestore. If the stored document contains a field such as __struct__ or a module name, and the reading code uses struct/2 with user-supplied data without whitelisting permitted keys, an attacker can inject a struct that executes code during construction. For example, a crafted payload could embed a module name that triggers initialization logic with side effects when the application calls apply/2 on functions derived from the deserialized data. Firestore’s flexible document model means these malicious fields persist and are served to other requests, amplifying the exposure across users and services.
Another vector involves polymorphic parameters where Firestore documents reference handlers by name. If Chi controllers deserialize a parameter like handler and dynamically invoke a module using Module.concat/1 or Code.eval_quoted/2, an attacker can supply a module path that leads to privilege escalation or data exfiltration. Even when using libraries that convert maps to structs, if the permitted list is not strict, new fields added by an attacker may be interpreted as functions or hooks during deserialization. Because Firestore indexes and queries rely on document fields, malicious payloads can be hidden inside commonly indexed keys, making detection harder without strict input validation and type checking before persistence.
The combination of Chi’s pattern-matching flexibility and Firestore’s schemaless storage creates a risk where untrusted data is both accepted and later reconstructed. Without rigorous validation, deserialization becomes a pathway for injection, remote code execution, or logic bypass. This aligns with OWASP API Top 10 #1 (Broken Object Level Authorization) when deserialized data is used to bypass access controls, and it can intersect with BFLA if Firestore rules do not properly constrain read and write access to sensitive document paths.
Firestore-Specific Remediation in Chi
Remediation focuses on strict validation, avoiding dynamic code execution, and constraining Firestore reads/writes. In Chi, prefer explicit changeset functions that whitelist permitted fields and never directly struct user input. Use Ecto changesets or a validation library to ensure only expected keys are present, and map them to normalized maps before storing in Firestore. Avoid storing or reconstructing Elixir structs with user-controlled __struct__ fields. If you must persist complex data, serialize to JSON strings or use deterministic maps with versioning, and validate on read.
When reading from Firestore, treat documents as untrusted. Do not use user-supplied values to select modules or functions for invocation. Instead, map document type fields to known handler modules via a whitelist. Below are concrete Chi code examples demonstrating secure handling.
| Scenario | Insecure Approach | Secure Approach |
|---|---|---|
| Creating a document from user input | params %{"amount" => 5, "__struct__" => "Malicious"} |> struct(UserSettings) |
params %{"amount" => 5} |> UserSettings.changeset() |> apply_action(:insert) |
| Selecting a handler based on a document field | handler = params["handler"] |> Module.concat() |> apply(:run, []) |
allowed = %{"email" => EmailHandler, "sms" => SmsHandler}; handler = allowed[params["handler"]]; handler.run() |
defmodule MyApp.Settings do
@permitted ~w(amount currency user_id)a
def changeset(params) do
%{}
|> Ecto.Changeset.cast(params, @permitted)
|> Ecto.Changeset.validate_required([:amount, :currency, :user_id])
|> Ecto.Changeset.cast_assoc(:profile, required: false)
end
end
# Writing to Firestore via a client wrapper that expects plain maps
params
|> MyApp.Settings.changeset()
|> case do
%{valid?: true, changes: changes} ->
Firestore.put("user_settings/#{changes.user_id}", Map.take(changes, @permitted))
%{valid?: false} ->
{:error, :invalid_params}
end
# Reading from Firestore and mapping to known handlers without dynamic invocation
with {:ok, doc} <- Firestore.get("user_settings/user_id_123"),
%{valid?: true, data: settings} <- MyApp.Settings.changeset(doc) do
handler = settings["channel"]
|> case do
"email" -> EmailHandler
"sms" -> SmsHandler
_ -> DefaultHandler
end
handler.deliver(settings)
else
_ -> {:error, :not_found}
end
Ensure Firestore security rules restrict write access to known fields and prevent updates to metadata keys that could be abused for deserialization. Combine this with Chi’s pipeline validation to guarantee that only clean, validated data reaches persistence layers.