Skip to content

CWE-628: Function Call with Incorrectly Specified Arguments

Overview

Function Call with Incorrectly Specified Arguments occurs when code invokes a function with the wrong argument count, order, type, or value. This is typically a coding-logic defect rather than an injection flaw, but when the affected function performs a security decision (authentication, authorization, buffer sizing, cryptographic operation), the mistake can bypass a security check, corrupt memory, or crash the process. It is most common with long positional-argument lists where several parameters share the same type.

A scanner will usually cite one of CWE-628's children rather than CWE-628 itself: CWE-683 (Function Call With Incorrect Order of Arguments), CWE-685 (Function Call With Incorrect Number of Arguments), CWE-686 (Function Call With Incorrect Argument Type), CWE-687 (Function Call With Incorrectly Specified Argument Value), or CWE-688 (Function Call With Incorrect Variable or Reference as Argument). None of the five has a page here, and the guidance below covers all of them, because the remediation differs by argument kind rather than by which child was cited. One concrete instance does have a page: CWE-560 (Use of umask() with chmod-style Argument) is a wrong argument value under CWE-687, and a umask finding reported against this page belongs there.

OWASP Classification

A06:2025 - Insecure Design

Risk

Medium-High: Depending on the function involved, incorrect arguments can cause buffer overflows (wrong size passed to a copy function), authentication bypass (an omitted MFA argument, so the MFA branch reads "not required" instead of "not provided"), permission bits taken from whatever occupied a stack slot (open() with O_CREAT and no mode argument), null pointer crashes, or a call that silently does nothing (a transposed memset that zeroes nothing and leaves the buffer holding its old contents).

Remediation Steps

Core Principle: Make it structurally hard to pass the wrong argument - use named parameters or a parameter object instead of long positional lists, and validate every argument at the function boundary.

Trace the Data Path

  • Source: The call site - often a security-relevant function invoked with several parameters of the same type (multiple strings, multiple booleans, multiple IDs).
  • Sink: The function body, which trusts argument position and type without verifying them.
  • Missing Control: No compiler/interpreter enforcement of argument identity, and no runtime validation inside the function to catch a swapped, missing, or wrong-typed value.

Use Named Parameters or a Parameter Object (Primary Defense)

Replace long positional argument lists with named parameters, keyword-only arguments, or a small parameter object/struct.

// VULNERABLE - positional arguments of the same type are easy to swap
function copyData(dest, src, size)
copyData(src, dest, size)   // swapped dest/src, silently wrong

// SECURE - named parameters bind each value to an explicit name
function copyDataSafe(params: { destination, source, maxSize })
copyDataSafe({ destination: dest, source: src, maxSize: size })

Why this works: the caller must state which value plays which role, so the two same-typed arguments can no longer be transposed without also changing their labels - which puts the mistake where a reviewer can see it instead of leaving it invisible in a positional list. The compiler is not the control here: copyDataSafe({ destination: src, source: dest, maxSize: size }) labels both values and still compiles, because a name binds an argument to a parameter and says nothing about whether the value belongs in that role. Distinct types for the two roles, or the validation below, are what reject a transposed value.

Where Named Parameters Do Not Exist (C and C++)

C has no named arguments, and this is where the classic findings live: transposed memcpy/memset arguments, a printf format string that disagrees with the values after it, and open() called with O_CREAT but no mode argument. POSIX declares open() variadic, so the missing mode is not a diagnosable error, and the new file's permission bits come from whatever occupied that stack slot. Three substitutes for the missing names:

  • Make the compiler's format checking fatal. GCC and Clang check printf-family calls against their format strings under -Wformat (enabled by -Wall); -Wformat=2 adds the non-literal-format checks, and -Werror=format / -Werror=format-security turn a mismatch into a build failure instead of a warning that scrolls past. Annotate your own printf-like wrappers with __attribute__((format(printf, m, n))) so calls through them are checked the same way. Without the attribute the compiler cannot tell which parameter is the format string, and the wrapper becomes a hole in the checking.
  • Give same-typed arguments different types. A std::span (C++20) or gsl::span carries the pointer and its length as one value, so there is no separate size argument left to transpose or to get wrong. A distinct wrapper type - a Length or Bytes newtype, an enum for a mode instead of a bare int - is the C++ equivalent of naming the parameter: a transposition stops compiling. In C, passing a small struct by value does the same job.
  • Enable the analyzer rules for the known transpositions. The swapped-memset case has a dedicated warning in GCC and Clang (-Wmemset-transposed-args, enabled by -Wall), and clang-tidy and the commercial analyzers ship equivalent checks. Turn them on rather than relying on review to spot a call that compiles cleanly.

Validate Every Argument at the Function Boundary (Defense in Depth)

Even with named parameters, validate type, nullability, and range inside the function itself, because a caller can still construct or pass a malformed value:

  • Reject null/None for required parameters instead of letting it propagate.
  • Check type/shape before use, especially for values crossing a language or serialization boundary (JSON, RPC, FFI).
  • Check numeric ranges and business-rule invariants (amount > 0, size <= buffer capacity) before acting on them.
  • For multi-boolean signatures (read, write, execute), prefer an options object or enum over adjacent booleans.

Use a Builder for Complex Construction

For functions or constructors with many parameters (more than four or five), a builder that validates on build() centralizes the checks and makes each value's purpose explicit at the call site, instead of relying on argument position.

Test with Malicious and Malformed Inputs

  • Swap two same-typed arguments and confirm the call fails validation instead of silently doing the wrong thing.
  • Omit a required argument / pass null and confirm a clear rejection, not an unchecked crash or a fallback that grants access.
  • Pass a wrong-typed value (string where a number is expected, or vice versa) and confirm it is rejected, not coerced into unexpected behavior.
  • Fuzz the function with randomized argument combinations and confirm every invalid combination raises a handled error rather than an unhandled exception.
  • Re-run the static/security scanner to confirm the finding is resolved.

Common Vulnerable Patterns

// VULNERABLE - five wrong calls; only some of them survive the toolchain
createFile(0644, "/tmp/file.txt")   // createFile(path, mode): wrong type per position -
                                    // rejected by any static type checker
authenticate("user", "pass")        // authenticate(username, password, mfaToken):
                                    // wrong count - survives only where arity is not enforced
memset(buffer, length, 0)           // memset(dest, value, count): transposed, all-integer,
                                    // compiles cleanly and writes nothing
printf("%s\n", mode)                // varargs: an int read as a pointer and dereferenced
hasPermission(null, resource)       // null: accepted or rejected by how the parameter is
                                    // declared, not by which language this is

Why this is vulnerable: the argument mistakes that matter are the ones the toolchain cannot see, and the first two are not among them. A static type system rejects createFile(0644, "/tmp/file.txt"), and C requires a diagnostic for passing an int where a const char * is expected - it only becomes a silent reinterpretation of an integer as an address after an explicit cast, or across a varargs or FFI boundary where no prototype constrains it, which is what printf("%s\n", mode) shows. The arity error is the same story: a compile error in Java, C#, C, C++ and Go, an ArgumentCountError or TypeError at runtime in PHP and Python, and only in JavaScript - or in any language where the parameter has a default - does mfaToken quietly become undefined and the MFA branch read "not required" instead of "not provided".

What survives compilation everywhere is the transposition of two arguments the compiler cannot tell apart: memset(buffer, length, 0) for memset(buffer, 0, length) zeroes nothing and leaves the buffer holding whatever was there, memcpy(src, dst, n) copies in the wrong direction whenever both pointers are non-const, and a format string that disagrees with its arguments is only checked if the diagnostics are switched on. hasPermission(null, resource) compiles too, wherever the parameter's declaration admits null - which is a property of that declaration rather than of the language. An untyped parameter always takes it, and so does a Java reference parameter whatever its type. Where the parameter carries a type the answer depends on how it was written: Kotlin accepts null for a String? and refuses it for a String, TypeScript under strictNullChecks draws the same line between string | null and string, PHP raises a TypeError before the body runs for stdClass $user and accepts it for ?stdClass $user - and does the same for a scalar type such as int even with strict_types off (measured on PHP 8.5.8) - while C# with nullable reference types enabled reports it as a warning rather than an error unless warnings are treated as errors. Rust has no null to pass at all. Where it does compile, the danger is not the crash but a handler above it that treats the exception as a permitted request.

Secure Patterns

// SECURE - named parameters plus boundary validation
function authenticate(params: { username, password, mfaToken? }) -> bool
    require params.username and params.password
    user = lookupUser(params.username)
    if not user or not verifyPassword(params.password, user.passwordHash):
        return false
    if user.mfaEnabled:
        require params.mfaToken            // cannot be silently omitted
        return verifyMfa(user, params.mfaToken)
    return true

Why this works: naming the parameters puts each value's role at the call site, where a transposition is visible rather than hidden in a positional list, and the explicit require checks turn a missing or malformed argument into an immediate, handled failure instead of an implicit bypass. MFA can no longer be skipped just because the caller forgot a positional argument.

Additional Resources