Skip to content

CWE-628: Function Call with Incorrectly Specified Arguments

Overview

Incorrect function arguments (wrong type, wrong order, wrong count, null when required) cause undefined behavior, security check bypass, buffer overflows, null pointer dereferences, and logic errors, often due to API misuse or type confusion.

OWASP Classification

A06:2025 - Insecure Design

Risk

Medium-High: Incorrect arguments cause buffer overflows (wrong buffer size), authentication bypass (swapped username/password), privilege escalation (wrong permission check), null pointer crashes, and logic errors enabling security bypasses.

Remediation Steps

Core principle: Call functions with correct argument types/order/count; enforce contracts to prevent memory/state corruption.

Locate Incorrect Function Arguments

When reviewing security scan results:

  • Find function calls: Look for functions with multiple parameters of same type
  • Check argument order: Functions where order matters (dest/src, user/password)
  • Identify type confusion: int vs string, null vs empty, array vs scalar
  • Look for missing validation: Functions accepting null when shouldn't
  • Check security functions: Authentication, authorization, crypto functions

Vulnerable patterns:

grep -r "strncpy\|memcpy\|copy" --include="*.c"  # Buffer functions
grep -r "authenticate\|authorize" --include="*.java"  # Auth functions
grep -r "transfer\|withdraw" --include="*.py"  # Financial functions

Use Type-Safe APIs and Named Parameters (Primary Defense)

// VULNERABLE - easy to swap arguments
void copy_data(char *dest, const char *src, size_t size);

// All same function signature - easy to mix up
copy_data(buffer, src, sizeof(buffer));  // OK
copy_data(src, buffer, sizeof(buffer));  // WRONG! Swapped dest/src
copy_data(buffer, sizeof(buffer), src);  // WRONG! size before src

// SECURE - use struct for named parameters
struct CopyParams {
    char* destination;
    const char* source;
    size_t max_size;
};

void copy_data_safe(const CopyParams& params) {
    if (params.destination == nullptr || params.source == nullptr) {
        return;  // Invalid
    }
    memcpy(params.destination, params.source, params.max_size);
}

// Usage - clear what each parameter is
CopyParams p = {
    .destination = buffer,
    .source = src,
    .max_size = sizeof(buffer)
};
copy_data_safe(p);  // Can't mix up parameter order

Python example:

# VULNERABLE - positional arguments
def set_permission(user, resource, read, write, execute):
    pass

# Hard to read, easy to swap booleans
set_permission(user, res, True, False, True)
set_permission(user, res, False, True, True)  # Swapped read/write!

# SECURE - keyword-only arguments (Python 3)
def set_permission(user, resource, *, read=False, write=False, execute=False):
    if not isinstance(user, User):
        raise TypeError("user must be User object")
    if not isinstance(resource, Resource):
        raise TypeError("resource must be Resource object")

    # Apply permissions
    resource.set_permissions(user, read, write, execute)

# Usage - crystal clear intent
set_permission(user, res, read=True, execute=True)
set_permission(user, res, write=True)  # Can't swap accidentally

Why this works: Named parameters make code self-documenting and prevent argument order mistakes. The compiler/interpreter enforces correct usage.

Validate All Arguments

def transfer_funds(from_account, to_account, amount):
    # Type validation
    if not isinstance(from_account, Account):
        raise TypeError("from_account must be Account object")
    if not isinstance(to_account, Account):
        raise TypeError("to_account must be Account object")
    if not isinstance(amount, (int, float)):
        raise TypeError("amount must be numeric")

    # Null/None validation
    if from_account is None or to_account is None:
        raise ValueError("Accounts cannot be None")

    # Range validation
    if amount <= 0:
        raise ValueError("Amount must be positive")
    if amount > from_account.balance:
        raise ValueError("Insufficient funds")

    # Business logic validation
    if from_account.is_frozen:
        raise ValueError("Source account is frozen")
    if to_account.is_frozen:
        raise ValueError("Destination account is frozen")

    # Perform transfer
    from_account.withdraw(amount)
    to_account.deposit(amount)
    log_transfer(from_account, to_account, amount)

Java example:

public void processUser(User user, String action, Map<String, Object> params) {
    // Null validation
    Objects.requireNonNull(user, "User cannot be null");
    Objects.requireNonNull(action, "Action cannot be null");

    // Empty check
    if (action.isEmpty()) {
        throw new IllegalArgumentException("Action cannot be empty");
    }

    // Type validation for params
    if (params == null) {
        params = new HashMap<>();
    }

    // Validate enum
    UserAction validAction;
    try {
        validAction = UserAction.valueOf(action.toUpperCase());
    } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException("Invalid action: " + action);
    }

    // Process with validated inputs
    user.performAction(validAction, params);
}

Use Strong Typing and Type Hints

// TypeScript - compile-time type checking
interface TransferParams {
    from: BankAccount;
    to: BankAccount;
    amount: number;
    currency: Currency;
}

function transferFunds(params: TransferParams): TransferResult {
    // TypeScript enforces types at compile time
    // Can't pass wrong types or wrong order
    return processTransfer(params);
}

// Usage - IDE autocomplete, compile-time validation
transferFunds({
    from: accountA,
    to: accountB,
    amount: 100.50,
    currency: Currency.USD
});

// This would fail at compile time:
transferFunds({
    from: accountA,
    to: 12345,  // ERROR: not a BankAccount
    amount: "100",  // ERROR: not a number
});

Python type hints:

from typing import Optional, List

def authenticate(username: str, password: str, mfa_token: Optional[str] = None) -> bool:
    """Authenticate user with optional MFA.

    Args:
        username: User's username (required)
        password: User's password (required)  
        mfa_token: MFA token (optional)

    Returns:
        True if authenticated, False otherwise
    """
    if not username or not password:
        raise ValueError("Username and password required")

    # Check credentials
    user = get_user(username)
    if not user or not verify_password(password, user.password_hash):
        return False

    # Check MFA if enabled
    if user.mfa_enabled:
        if not mfa_token:
            raise ValueError("MFA token required")
        return verify_mfa(user, mfa_token)

    return True

Use Builder Pattern for Complex Function Calls

// Complex function with many parameters - error-prone
public User createUser(String username, String email, String password,
                       Role role, boolean active, boolean verified,
                       Date createdAt, String notes) {
    // Easy to mix up parameters
}

// BETTER - Builder pattern
public class UserBuilder {
    private String username;
    private String email;
    private String password;
    private Role role = Role.USER;  // Default
    private boolean active = true;
    private boolean verified = false;
    private Date createdAt = new Date();
    private String notes = "";

    public UserBuilder withUsername(String username) {
        this.username = username;
        return this;
    }

    public UserBuilder withEmail(String email) {
        this.email = email;
        return this;
    }

    public UserBuilder withPassword(String password) {
        this.password = password;
        return this;
    }

    public UserBuilder withRole(Role role) {
        this.role = role;
        return this;
    }

    public User build() {
        // Validation
        if (username == null || username.isEmpty()) {
            throw new IllegalArgumentException("Username required");
        }
        if (email == null || !isValidEmail(email)) {
            throw new IllegalArgumentException("Valid email required");
        }
        if (password == null || password.length() < 8) {
            throw new IllegalArgumentException("Password must be at least 8 characters");
        }

        return new User(username, email, password, role, active, verified, createdAt, notes);
    }
}

// Usage - clear and self-documenting
User user = new UserBuilder()
    .withUsername("john.doe")
    .withEmail("john@example.com")
    .withPassword("SecurePass123!")
    .withRole(Role.ADMIN)
    .build();

Test for Argument Errors

Unit tests:

import pytest

def test_transfer_with_wrong_argument_types():
    # Test type validation
    with pytest.raises(TypeError):
        transfer_funds("not_an_account", account_b, 100)

    with pytest.raises(TypeError):
        transfer_funds(account_a, account_b, "not_a_number")

def test_transfer_with_swapped_arguments():
    # Ensure swapping doesn't work silently
    initial_a = account_a.balance
    initial_b = account_b.balance

    # Correct order
    transfer_funds(from_account=account_a, to_account=account_b, amount=50)
    assert account_a.balance == initial_a - 50
    assert account_b.balance == initial_b + 50

def test_transfer_with_null_arguments():
    with pytest.raises(ValueError):
        transfer_funds(None, account_b, 100)

    with pytest.raises(ValueError):
        transfer_funds(account_a, None, 100)

def test_transfer_with_invalid_amount():
    with pytest.raises(ValueError):
        transfer_funds(account_a, account_b, -50)  # Negative

    with pytest.raises(ValueError):
        transfer_funds(account_a, account_b, 0)  # Zero

    with pytest.raises(ValueError):
        transfer_funds(account_a, account_b, 10000)  # Exceeds balance

Fuzzing for argument errors:

import hypothesis
from hypothesis import given, strategies as st

@given(
    amount=st.integers(),
    account_a=st.one_of(st.none(), st.builds(Account)),
    account_b=st.one_of(st.none(), st.builds(Account))
)
def test_transfer_handles_all_inputs(account_a, account_b, amount):
    try:
        transfer_funds(account_a, account_b, amount)
    except (TypeError, ValueError) as e:
        # Expected for invalid inputs
        pass
    except Exception as e:
        # Unexpected exception - test fails
        pytest.fail(f"Unexpected exception: {e}")

Common Vulnerable Patterns

Wrong Argument Order

// Definition: void create_file(const char *path, mode_t mode)
create_file(0644, "/tmp/file.txt");  // SWAPPED!

// strncpy(dest, src, size)
strncpy(src, dest, len);  // WRONG ORDER

Why is this vulnerable: When argument order is reversed, the function performs the opposite of the intended operation. In the create_file example, passing the numeric mode (0644) as the first argument instead of the path causes the function to interpret the number as a memory address, likely causing a crash or security violation. In strncpy, swapping source and destination overwrites the wrong buffer, potentially corrupting the source data or causing buffer overflow if the destination buffer is smaller than the source.

Wrong Argument Type

# Expects string, gets int

open_file(1234)  # Should be open_file("file.txt")

# Expects int, gets string

set_timeout("30")  # Should be set_timeout(30)

Why is this vulnerable: Type mismatches cause unpredictable behavior depending on the language. In dynamically-typed languages like Python, passing an integer to open_file() when it expects a string path may cause the function to attempt opening a file descriptor by number, potentially accessing an unintended file. Passing a string to set_timeout() when it expects a number may either cause a runtime error (safer) or silently convert the string to an unexpected value, potentially bypassing timeout protections.

Wrong Argument Count

// Missing required argument
function authenticate(username, password, mfaToken) {
    // mfaToken required
}
authenticate("user", "pass");  // Missing MFA!

Why is this vulnerable: When required security parameters are omitted, the function receives undefined (JavaScript) or None (Python) for missing arguments. If the function doesn't validate that mfaToken is present, authentication may succeed without MFA verification, completely bypassing two-factor authentication. This enables attackers to authenticate with only username/password even when MFA is supposedly required.

Null/None When Not Allowed

public boolean hasPermission(User user, Resource resource) {
    return user.getRoles().contains(resource.getRequiredRole());
}

hasPermission(null, resource);  // NullPointerException!

Why is this vulnerable: Passing null when a valid object is expected causes a NullPointerException when the method attempts to call user.getRoles(). While this typically crashes the application (denial of service), in some cases the exception may be caught and handled incorrectly, potentially defaulting to granting permission. Additionally, if the function doesn't validate parameters, null values can propagate through the system, causing security checks to be bypassed further downstream.

Secure Patterns

Builder Pattern

User user = new UserBuilder()
    .withUsername("john")
    .withEmail("john@example.com")
    .withRole(Role.ADMIN)
    .build();

Why this works: The Builder pattern makes complex object construction self-documenting and error-resistant - instead of new User("john", "john@example.com", "hashed_pwd", Role.ADMIN, true, false, new Date(), "") where argument order mistakes are easy, named methods like .withUsername("john") make each value's purpose explicit and prevent argument transposition. Method chaining (.withUsername().withEmail().build()) provides a fluent API that reads naturally and enables partial construction with sensible defaults for omitted fields. build() validation centralizes all parameter checks in one place, ensuring no invalid objects can exist and enforcing invariants at construction time.

Constructor Validation

class Rectangle {
    Rectangle(int width, int height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException(
                "Dimensions must be positive");
        }
        this.width = width;
        this.height = height;
    }
}

Why this works: Constructor validation ensures no invalid objects can exist - by checking parameters at construction time and throwing exceptions for invalid values, the class maintains its invariants. Once a Rectangle object exists, you can trust that width and height are positive, eliminating the need for defensive checks throughout the codebase.

Optional for Nullable Returns

public Optional<User> findUser(String id) {
    // Caller must handle absence
}

Why this works: Optional<User> forces null handling - instead of returning null (which causes NullPointerException if unchecked), Optional requires callers to explicitly handle absence via .isPresent(), .orElse(), or .orElseThrow(), preventing null pointer crashes. The type system enforces that developers consider the "not found" case at compile time.

TypeScript Type Safety

function processOrder(
    orderId: number,
    items: OrderItem[],
    options: { priority?: boolean; notify?: boolean }
): Promise<OrderResult> {
    // Types enforced at compile time
}

Why this works: TypeScript's type system catches argument errors at compile time before code runs - passing a string where a number is expected (processOrder("123", items, {})) fails compilation, and IDE autocomplete shows parameter names/types, reducing mistakes. Named parameters in TypeScript ({ priority?: boolean }) make boolean flags readable - instead of processOrder(123, items, true, false) where boolean meanings are unclear, { priority: true, notify: false } is self-explanatory.

Security Checklist

  • All functions validate argument types
  • Null/None arguments rejected where required
  • Named parameters used for functions with >3 arguments
  • Type hints/annotations added (Python, TypeScript)
  • Builder pattern used for complex object creation
  • Security-critical functions validate all inputs
  • Unit tests cover wrong types, null, swapped arguments
  • Fuzzing tests catch unexpected argument combinations

Additional Resources