Semmle 1.21
Skip to end of metadata
Go to start of metadata

Name: NULL application name with an unquoted path in call to CreateProcess

Description: Calling a function of the CreateProcess* family of functions, where the path contains spaces, introduces a security vulnerability.

ID: cpp/unsafe-create-process-call

Kind: problem

Severity: error

Precision: medium

Query: UnsafeCreateProcessCall.ql
/**
 * @name NULL application name with an unquoted path in call to CreateProcess
 * @description Calling a function of the CreateProcess* family of functions, where the path contains spaces, introduces a security vulnerability.
 * @id cpp/unsafe-create-process-call
 * @kind problem
 * @problem.severity error
 * @precision medium
 * @msrc.severity important
 * @tags security
 *       external/cwe/cwe-428
 *       external/microsoft/C6277
 */
 
import cpp
import semmle.code.cpp.dataflow.DataFlow
import semmle.code.cpp.dataflow.DataFlow2

predicate isCreateProcessFunction(FunctionCall call, int applicationNameIndex, int commandLineIndex) {
  ( 
    call.getTarget().hasGlobalName("CreateProcessA") 
    and applicationNameIndex = 0 
    and commandLineIndex = 1
  ) or ( 
    call.getTarget().hasGlobalName("CreateProcessW") 
    and applicationNameIndex = 0
    and commandLineIndex = 1
  ) or ( 
    call.getTarget().hasGlobalName("CreateProcessWithTokenW") 
    and applicationNameIndex = 2
    and commandLineIndex = 3
  ) or ( 
    call.getTarget().hasGlobalName("CreateProcessWithLogonW") 
    and applicationNameIndex = 4
    and commandLineIndex = 5
  ) or ( 
    call.getTarget().hasGlobalName("CreateProcessAsUserA") 
    and applicationNameIndex = 1
    and commandLineIndex = 2
  ) or ( 
    call.getTarget().hasGlobalName("CreateProcessAsUserW") 
    and applicationNameIndex = 1
    and commandLineIndex = 2
  ) 
}
/**
 * A function call to CreateProcess (either wide-char or single byte string versions)
 */
class CreateProcessFunctionCall extends FunctionCall {
  CreateProcessFunctionCall() {
    isCreateProcessFunction( this, _, _)
  }
  
  int getApplicationNameArgumentId() {
    isCreateProcessFunction( this, result, _)
  }
  
  int getCommandLineArgumentId() {
    isCreateProcessFunction( this, _, result)
  }
}
 
/**
 * Dataflow that detects a call to CreateProcess with a NULL value for lpApplicationName argument
 */
class NullAppNameCreateProcessFunctionConfiguration extends DataFlow::Configuration {
  NullAppNameCreateProcessFunctionConfiguration() {
    this = "NullAppNameCreateProcessFunctionConfiguration"
  }

  override predicate isSource(DataFlow::Node source) {
    source.asExpr() instanceof NullValue
  }

  override predicate isSink(DataFlow::Node sink) {
    exists( 
      CreateProcessFunctionCall call, Expr val |
      val = sink.asExpr() |
      val = call.getArgument(call.getApplicationNameArgumentId()) 
    )
  }
}

/**
 * Dataflow that detects a call to CreateProcess with an unquoted commandLine argument
 */
class QuotedCommandInCreateProcessFunctionConfiguration extends DataFlow2::Configuration {
  QuotedCommandInCreateProcessFunctionConfiguration() {
    this = "QuotedCommandInCreateProcessFunctionConfiguration"
  }

  override predicate isSource(DataFlow2::Node source) {
    exists( string s |
      s = source.asExpr().getValue().toString()
      and
      not isQuotedOrNoSpaceApplicationNameOnCmd(s) 
    ) 
  }
 
  override predicate isSink(DataFlow2::Node sink) {
    exists( 
      CreateProcessFunctionCall call, Expr val |
      val = sink.asExpr() |
      val = call.getArgument(call.getCommandLineArgumentId()) 
    )
  }
}

bindingset[s]
predicate isQuotedOrNoSpaceApplicationNameOnCmd(string s){
    s.regexpMatch("\"([^\"])*\"(\\s|.)*") // The first element (path) is quoted
    or
    s.regexpMatch("[^\\s]+") // There are no spaces in the string
}

from CreateProcessFunctionCall call, string msg1, string msg2
where
  exists( Expr source, Expr appName,
    NullAppNameCreateProcessFunctionConfiguration nullAppConfig |
    appName = call.getArgument(call.getApplicationNameArgumentId())
    and nullAppConfig.hasFlow(DataFlow2::exprNode(source), DataFlow2::exprNode(appName))
    and msg1 = call.toString() + " with lpApplicationName == NULL (" + appName + ")"
  )
  and
  exists( Expr source, Expr cmd,
    QuotedCommandInCreateProcessFunctionConfiguration quotedConfig |
    cmd = call.getArgument(call.getCommandLineArgumentId())
    and quotedConfig.hasFlow(DataFlow2::exprNode(source), DataFlow2::exprNode(cmd))
    and msg2 = " and with an unquoted lpCommandLine (" + cmd + ") introduces a security vulnerability if the path contains spaces."
  )
select call, msg1 + " " + msg2

This query indicates that there is a call to a function of the CreateProcess* family of functions, which introduces a security vulnerability.

Recommendation

Do not use NULL for the lpApplicationName argument to the CreateProcess* function.

If you pass NULL for lpApplicationName, use quotation marks around the executable path in lpCommandLine.

Example

In the following example, CreateProcessW is called with a NULL value for lpApplicationName, and the value for lpCommandLine that represent the application path is not quoted and has spaces in it.

If an attacker has access to the file system, they can elevate privileges by creating a file such as C:\Program.exe that will be executed instead of the intended application.

STARTUPINFOW si;
PROCESS_INFORMATION pi;

// ... 

CreateProcessW(                           // BUG
    NULL,                                 // lpApplicationName
    (LPWSTR)L"C:\\Program Files\\MyApp",  // lpCommandLine
    NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

// ...

To fix this issue, specify a valid string for lpApplicationName, or quote the path for lpCommandLine. For example:

(LPWSTR)L"\"C:\\Program Files\\MyApp\"", // lpCommandLine

References