Skip to content

S01:2023 - Injection đź’‰

Overview

Snaps communicate with the outside world via defined API. A common finding in security reviews is that data crossing the trust-boundary from dApps to a Snap is not or insufficiently validated. With exposure ranging from UI/UX injections, the bypassing of security controls, the inclusion of malicious data in requests, queries, and commands, this category is estimated high risk, taking the lead as #1 in the Snap Security Top 10.

Description

This segment underscores the importance of validating user inputs and establishing robust mechanisms to securely handle sensitive data, effectively mitigating potential vulnerabilities. Robust input validation and thorough output sanitization are integral aspects of crafting a secure wallet extension. This involves scrutinizing and cleansing user-provided data to guarantee conformity with expected formats, lengths, and types.

  • The Snap fails to validate, filter, or sanitize dApp, user-supplied, or untrusted 3rd party data effectively.
  • The RPC handlers are not rejecting invalid RPC parameters.
  • Lax or too broad filters allow for the bypassing of checks.
  • Missing type and bounds checks, as well as safe application limits are not enforced.
  • Untrusted data is directly utilized or concatenated, resulting in the inclusion of potentially malicious data in dynamic queries, commands, and requests.
  • Data is not properly sanitized before use within other contexts (Markdown, <text>, HTTP-Request URIs).
  • Security features that prevent the evaluation of untrusted data are not enabled by default.

How to Prevent

  • Define a strict data model and use safe standard libraries for data validation.
  • Switch from JavaScript to TypeScript for static compile-time type checking.
  • Enforce runtime type checks for untrusted data in JavaScript and TypeScript.
  • Enforce strict data validation for all runtime data structures (RPC payloads, services responses, etc.).
  • Check for side-effects in UI components (i.e., do they allow contextual information like Markdown).
  • Sanitize output for use in different contexts: Markdown Sanitize for <text> components; URL-escape for use in HTTP requests.

Example Misuse Scenarios

Scenario #1: Markdown

Markdown Injection

A Snap displays unfiltered data from a dApps RPC request with the <text> UI component. The <text> component by default renders a limited Markdown instruction set. By providing Markdown stylized fragments in the request parameter, the dApp injects Markdown to change the appearance of the Snap's dialog potentially luring the user into performing actions they otherwise would not choose.

Markdown Injection

export const onRpcRequest: OnRpcRequestHandler = async ({
  origin,
  request,
}) => {
  switch (request.method) {
    case "hello": {
       const address = request.params.address || "0x0";

       return snap.request({
            method: 'snap_dialog',
            params: {
            type: 'alert',
            content: panel([
                heading('Address added'),
                text('Following addresses will receive notifications:'),
                divider(),
                text(`${address}`),  // INSECURE
            ]),
            },
        });
    }

PoC:

await window.ethereum?.request({
      method: "wallet_invokeSnap",
      params: {
        snapId: "local:http://localhost:8080",
        request: { method: 'hello', params: { address: "Hi 🙌\n\n 🔸 **boom**" } },
      }});

Scenario #2: Control Characters

A Snap displays unfiltered data from a HTTP response to a 3rd party service with the <text> UI component. The <text> component by default renders control characters. For example, by injecting CR-LF characters in the HTTP Response, the 3rd party service injects control characters to change the appearance of the Snap's dialog potentially hiding information or luring the user into performing actions they otherwise would not choose.

Scenario #3: Validation on Transformed Data

A Snap implements a dialog that asks the user to confirm if they want to sign a certain transaction. In order to provide the user with more information, the Snap simulates the transaction with a 3rd party library to provide human readable context to the user.

The transaction data is sent to the Snap from a dApp that maliciously provides a JSON encoded text value that includes multiple overlapping keys. Input validation on the JSON-encoded data is performed after converting it to a JavaScript object, hiding the fact that multiple same keys were used in the struct. After validation, the original malicious JSON payload is forwarded to the 3rd party service which may—depending on the language/parser used—misinterpret the transaction data and output a completely different transaction simulation outcome.

The dishonest dApp misled the user by providing a specially crafted transaction object to trick the user into signing-off a transaction based on a transaction simulation that does not represent the real transaction executed.

Scenario #4: Too-Broad Regular Expression

A Snap allows modification of administrative settings (e.g. API-keys, cccounts) exclusively from a specific trusted origin offered by the snap developers. The Snap validates the origin parameter in the main RPC handler. However, the RegEx validation is too broad, allowing non-production, beta, auto-testing instanced living on the same domain to administer user Snaps. Non-production environments may have different security criteria and therefore be more susceptible to compromise. A malicious developer or someone compromising one of the environments may lure a user into navigating to a compromised subdomain that is allowed to read/write administrative settings for the Snap.

Scenario #5: Non-Production Environments

Lax validation of the origin field in RPC call parameters allows non-production or debug environments (localhost*) to interact with a published Snap in production.

Scenario #6: Spread operator

Using the JavaScript spread operator on untrusted input may allow a dApp or 3rd party to overwrite fields.

case Methods.ConnectApp:
        assert(request.params, connectWalletSchema);
        return await connectApp({origin, snap, ...request.params}); // INSECURE

Here, request.params.origin would overwrite the original origin field.

Scenario #7: Type Confusion

RPC parameter types are not strictly checked at runtime, which may expose the application to type confusion vulnerabilities due to the untrusted nature of the data passed through them.

if (typeof params.serializeConfig !== 'object' &&
    typeof params.serializeConfig.requireAllSignatures !== 'boolean' &&
    typeof params.serializeConfig.verifySignatures !== 'boolean'
    ) {
    throw new Error();

Scenario #8: Assuming TypeScript-Enforced Types

TypeScript primarily provides static type checking during the development phase, which helps catch potential type-related errors at compile time. However, it's important to note that TypeScript's type checking does not extend to runtime, meaning that the actual execution of the code at runtime may not guarantee the expected types due to JavaScript's dynamic nature.