CWE-676: Use of Potentially Dangerous Function
Overview
Some standard library functions are safe to use, but only if the caller upholds an obligation the function itself doesn't enforce: bounding a copy to the destination's real size, keeping attacker-influenced text out of a command shell, keeping untrusted text out of a dynamic code evaluator. MITRE lists C and C++ under Applicable Platforms, and that is where the classic examples live. None of strcpy, sprintf or system is deprecated; each has a documented safe calling convention, and each is easy to call in a way that isn't safe.
The shape is not confined to those two languages, which is why this page has a Python page beside it. eval(), os.system() and subprocess(..., shell=True) are current, supported API with a narrow correct use and an obvious wrong one. That is the same bargain strcpy offers, with arbitrary code execution rather than a buffer overflow at the end of it.
Relationship to Other CWEs
This weakness sits between two related ones that are easy to confuse with it. The entries below set out the scope of each:
- CWE-676 (this page) - covers current, standard functions that have a documented safe calling convention the function itself doesn't enforce, so the obligation falls on the caller.
- CWE-242 (Use of Inherently Dangerous Function) - covers functions with no safe calling convention at all.
gets()is the canonical example: it cannot be bounded no matter how it's called. CWE-676 covers functions that do have a safe calling convention, but where the unsafe convention is just as easy to reach for. - CWE-477 (Use of Obsolete Function) - covers functions the language or platform has superseded outright, regardless of how carefully they're called. CWE-676 functions are still current, standard API, so the risk is in the call site rather than in the function's status.
OWASP Classification
A06:2025 - Insecure Design
Risk
High: A dangerous function called without its safety obligations upheld produces the same outcomes as the underlying weakness it exposes - buffer overflow from an unbounded copy, command injection from unsanitized shell arguments, or arbitrary code execution from evaluating untrusted text.
Remediation Steps
Core Principle: Prefer an API that enforces the safety obligation itself - a bounded copy, a parameterized command, a restricted evaluator - over a function that only stays safe as long as every caller remembers to check first.
Trace the Data Path
- Source: Any call to a function on the language's "use with care" list (bounds-unchecked copies, shell/command execution, dynamic code evaluation).
- Sink: The buffer, shell, or interpreter the function writes to or invokes.
- Missing control: No bound on the copy length, no separation between command and arguments, or no restriction on what the evaluated text can do.
Replace With a Safe-by-Construction Alternative (Primary Defense)
Prefer a function whose signature makes the unsafe call impossible to write, rather than one that merely documents a safe convention:
// SECURE - pseudo-code
copy_bounded(dest, source, dest_capacity) // capacity is a required argument, not caller discipline
run_command(program, ["--", untrusted_arg]) // a list, so no shell parses it - and "--" so the
// program does not parse it as an option either
Ban the Dangerous Function via Tooling
Where a safe-by-construction replacement doesn't exist yet, enforce the safe calling convention with a compiler warning, linter rule, or static analysis check that flags every call site, rather than relying on manual review to catch a missing bounds check.
Add Validation Where Replacement Isn't Possible (Defense in Depth)
If a dangerous function must remain, validate every input against the obligation it depends on before the call: confirm the source can't exceed the destination's capacity, confirm the command has no attacker-controlled shell metacharacters, and reject or separate any argument that could be read as an option. Treat this as a stopgap rather than the fix.
Test with Malicious Inputs
- Oversized input for any bounds-sensitive call, to confirm it's rejected or truncated rather than overflowing.
- Shell metacharacters (
| & $() \) in any value that reaches a command-execution call, to confirm they have no special effect. - A leading-hyphen value (
-v,--help, or an option the target program actually accepts) in the same place, to confirm it arrives as an operand rather than as a flag. This is the check the list form alone does not satisfy, and it passes only where an end-of-options separator or an equivalent guard is present. A suite that tests only metacharacters reports the argument list as sufficient when it is half the fix. - Re-scan or re-run static analysis to confirm no unguarded calls to the dangerous function remain. Note what the rule matches: a scanner that flags shell invocation will pass a list-form call whose argument is an unvalidated option or path.
Common Vulnerable Patterns
// VULNERABLE - pseudo-code
copy_unbounded(dest, source) // no destination capacity checked - overflow if source is longer
run_shell("cat " + user_supplied_name) // shell interprets metacharacters in user_supplied_name
Why this is vulnerable: each of these functions carries a safety obligation that its own signature gives the caller no way to express. copy_unbounded takes no destination capacity, so "use it carefully" means maintaining an invariant that lives somewhere other than the call - in the declaration of dest, possibly in another file, and true only until someone changes it. run_shell takes a string that a shell will parse, so the distinction the caller cared about - this part is command, that part is data - has to survive a round trip through a syntax with no way to represent it.
That is why the remedy is replacement rather than care. A bounded copy that takes the destination's size, or a process API that takes an argument array, moves the obligation into the call itself where the compiler and the reader can both see it.
It also means these findings have to be judged rather than counted. The scanner matches a function name, and the name does not say whether the obligation was met - an unbounded copy of a string literal into a buffer declared from the same constant is safe today, and its real risk is that it will not stay safe when someone edits one of the two. Recording that as accepted with the reason is a legitimate outcome; leaving it because it looks fine is not the same thing.
Secure Patterns
// SECURE - pseudo-code
copy_bounded(dest, source, dest_capacity)
// argument passed directly to the process, never through a shell - AND "--" so a
// value beginning with "-" is read as an operand rather than as an option
run_command("cat", ["--", user_supplied_name])
Why this works: A bounded copy takes the destination's capacity as a required argument, so there's no call site where a caller can forget to check it. Passing arguments as a list to a process launcher (rather than building a shell command string) means the shell never parses the argument, so metacharacters in it have no special meaning.
The -- is the second half of the fix, not a refinement of it. What the argument list does not do is decide how the target program reads what it receives: without the separator, a value starting with - arrives as an option rather than a filename, so an attacker who controls user_supplied_name can turn it into a flag the caller never intended. Removing the shell removes command injection; only the separator stops the argument being read as syntax by the program itself.
Neither addresses what the value means once accepted as an operand. A path is still a path, so where the argument names a file, resolve it against a known base directory as well - that is CWE-22, and no process API fixes it.
Language-Specific Guidance
- C - unbounded string and formatting functions, unsanitized
system()/popen()calls, and their bounds-checked/parameterized replacements - Python -
eval()/exec(),os.system()andsubprocesswithshell=True, and their safe replacements