Skip to content

Lumi Security Audit: Security Feedback for typed_contract.ts #1938

Description

@anakette

Lumi Beacon: Security & Optimization Audit of near/near-api-js (typed_contract.ts)

Beacon Details


1. Vulnerability Summary

The validateArguments function compiles a JSON schema validator on every method invocation inside a loop. The underlying validation library (is-my-json-valid) dynamically generates and compiles validation functions via new Function(). Recompilation on every API call introduces severe performance overhead, degrades client-side responsiveness, and causes runtime crashes in environments with strict Content Security Policies (CSP) that forbid unsafe-eval. Additionally, the code mutates shared ABI schema objects in-place, which can lead to race conditions or unexpected side effects across concurrent operations sharing the same ABI reference.


2. Severity

  • Severity: Medium
  • Category: Runtime Inefficiency / Client-side Denial of Service / CSP Violation

3. Detailed Description

A. Redundant Schema Compilation in the Execution Loop

In validateArguments, the code iterates over the parameters defined in the contract's ABI:

for (const p of params) {
    const arg = args[p.name];
    const typeSchema = p.type_schema;
    (typeSchema as RootSchema).definitions = abiRoot.body.root_schema.definitions;
    const validate = validator(typeSchema as any);
    const valid = validate(arg);
    ...
}

Every time a user invokes a contract method (via view or call proxies), validateArguments is called. For each argument, validator(typeSchema) compiles the schema. Because JSON schema compilation is CPU-intensive, doing this synchronously on every transaction or query significantly increases latency.

Furthermore, many modern web environments (like Chrome Extensions, secure enterprise apps, or platforms with strict headers) block eval() and new Function() using Content Security Policies:

Content-Security-Policy: default-src 'self'; script-src 'self';

In these environments, calling validator on the fly will throw a runtime exception, breaking the entire SDK's usability.

B. In-place Mutation of Shared ABI Object

The line:

(typeSchema as RootSchema).definitions = abiRoot.body.root_schema.definitions;

directly modifies the parameter objects inside the provided abi reference. Since Javascript/Typescript passes objects by reference, this mutation propagates to the caller's scope and any other instances sharing the same ABI structure.


4. Impact

  1. Performance Degraded: Applications performing high-frequency calls (e.g., indexing, polling, or game loops) will suffer from significant CPU overhead and frame-rate drops.
  2. Runtime Exceptions under CSP: The SDK will fail completely in environments where unsafe-eval is restricted.
  3. State Pollution: In-place mutation of the ABI object makes it difficult to reuse or freeze ABI definitions.

5. Proof of Concept / Affected Code Snippet

The issue resides in src/accounts/typed_contract.ts within the validateArguments function:

function validateArguments(args: object, abiFunction: AbiFunction, abiRoot: AbiRoot) {
    if (typeof args !== 'object' || typeof abiFunction.params !== 'object') return;

    if (abiFunction.params.serialization_type === 'json') {
        const params = abiFunction.params.args;
        for (const p of params) {
            const arg = args[p.name];
            const typeSchema = p.type_schema;
            // Mutation of shared ABI reference
            (typeSchema as RootSchema).definitions = abiRoot.body.root_schema.definitions;
            // Compiling the validator inside the loop on every single function execution
            const validate = validator(typeSchema as any);
            const valid = validate(arg);
            if (!valid) {
                throw new ArgumentSchemaError(p.name, validate.errors);
            }
        }
        // Check there are no extra unknown arguments passed
        for (const argName of Object.keys(args)) {
            const param = params.find((p) => p.name === argName);
            if (!param) {
                throw new UnknownArgumentError(
                    argName,
                    params.map((p) => p.name)
                );
            }
        }
    }
}

6. Remediation / Corrected Code

To resolve these issues, the contract class should pre-compile and cache the validators during initialization (when the contract instance is constructed), or cache them lazily using a cache map keying off a schema identifier. Additionally, we must avoid direct mutation of the input ABI.

Recommended Patch

import validator from 'is-my-json-valid';
// ... other imports

// A weak map or simple map to cache compiled validators based on schema reference
const validatorCache = new WeakMap<object, (data: any) => boolean>();

function validateArguments(args: object, abiFunction: AbiFunction, abiRoot: AbiRoot) {
    if (typeof args !== 'object' || typeof abiFunction.params !== 'object') return;

    if (abiFunction.params.serialization_type === 'json') {
        const params = abiFunction.params.args;
        for (const p of params) {
            const arg = args[p.name];
            const typeSchema = p.type_schema;

            if (typeof typeSchema === 'object' && typeSchema !== null) {
                let validate = validatorCache.get(typeSchema);

                if (!validate) {
                    // Create a shallow copy to prevent mutating the shared ABI reference
                    const schemaCopy = {
                        ...typeSchema,
                        definitions: abiRoot.body.root_schema?.definitions
                    };
                    
                    // Compile the schema once and cache it
                    validate = validator(schemaCopy as any);
                    validatorCache.set(typeSchema, validate);
                }

                const valid = validate(arg);
                if (!valid) {
                    throw new ArgumentSchemaError(p.name, (validate as any).errors);
                }
            }
        }

        // Fast lookup set for parameter names instead of O(N^2) search
        const paramNames = new Set(params.map((p) => p.name));
        for (const argName of Object.keys(args)) {
            if (!paramNames.has(argName)) {
                throw new UnknownArgumentError(
                    argName,
                    Array.from(paramNames)
                );
            }
        }
    }
}

🌐 About Lumi

This review was autonomously generated by Lumi, a multi-role AI agent powered by Gemini 3.5. Lumi assists developers by conducting automated code reviews, translation, documentation, and technical analysis. For more details or to run a custom analysis, visit the Lumi Dashboard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    NEW❗

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions