September 23, 2020

The Grey area of software security - whose responsibility is it?

Jaroslav Lobačevski

Security is a complex area. One software component may break the assumptions made by another component and it is not always clear who should fix the code to remediate the security implications.

That was the case in a local privilege elevation vulnerability found in the Windows version of installer for Composer - a package manager for PHP. A fix was released with version 6.0.0 and the vulnerability was assigned CVE-2020-15145.

By default, Composer for Windows would install its executables and libraries to C:\ProgramData\ComposerSetup\bin and subsequently add this directory to the system PATH environment variable. The Bin folder permissions were quite relaxed: all local users were allowed to modify and create files in the folder.

There were two local privilege elevation scenarios with different impacts:

  1. Regular users could modify the existing composer.bat batch script in order to get command execution in an elevated privilege context when Composer is run by an Administrator.

  2. Regular users could create a specially-crafted Dynamic-Link Library (DLL) in the Bin folder in order to get Local System privileges. Since the directory is added to system PATH, any application dynamically loading a DLL would search this directory when the DLL can not be found in other locations in the search path. This is commonly referred to as a "DLL planting" vulnerability.

Here, Regular user is used to describe the required access rights to the target folder. However, it could have been any malicious application looking for an opportunity to persist itself with privileges when run by an unsuspecting user.

From a user perspective, the PATH environment variable contains a list of file system locations that are searched for an executable file when the user starts it from a command line or a script without providing a full path to the executable. Adding a frequently run executable to the search path is very convenient as the user no longer has to provide a full path to the target executable to launch it. There is a user specific PATH environment variable that is limited to the user session as well as a system-wide PATH.

From a developer perspective, the PATH variable is important when using Run-Time Dynamic Linking. It is typically used in the form of a call to the LoadLibray or LoadLibraryEx function with a DLL name. Then Windows uses its heuristics to find the requested file. As a last step in the search order, Windows looks for the requested DLL in locations specified by the PATH. There are different reasons to use Run-Time linking. One of them is when the DLL is not available on all versions of the operating system.

If a program running in a privileged context fails to find a DLL, it can potentially be exploited by putting a malicious file with a matching DLL name into one of the locations listed in the PATH. Any PATH included location without proper access control permissions is a gift to a malicious user as it allows the loading of arbitrary attacker controlled DLL's into a potentially privileged application.

While Microsoft provides multiple options for developers to restrict the search order and dynamically load a DLL in a safe manner, DLL planting vulnerabilities that result in an escalation to Local System privileges are still not rare.

In the case of the Composer vulnerability, it is easy to assume the responsibility for security rests with both sides. Composer introduced a vulnerability by placing an insecure directory into the system PATH. From a Microsoft Windows perspective this is not considered an issue since system PATH entries are explicitly not supposed to be world writable. One could argue that Windows should ensure that all privileged system services load DLLs from base system directories only, but at the deploy scale of Microsoft Windows this solution has the risk of introducing significant regression issues for custom and third party applications. Concerns about unintended side-effects on third party application availability and stability make system level mitigation discussions much more nuanced.

Having said that, systems security remains a shared responsibility. Applications should do their best not to introduce vulnerabilities into the operating environment, whereas systems level security is responsible for reducing the impact of application level vulnerabilities on the system as a whole. Any security assumptions based on systems level mitigations should always be verified at the application level, where possible.

Confusion about remediation responsibility can lead to a situation where no one side would agree to fix the vulnerability. The creators of Composer application could argue that the local user is trusted and relaxed permissions are needed for a frictionless usage, while the creators of applications running at elevated privileges could argue that the assumption is that all files in the system PATH are secured and trusted. Luckily in this case Composer's developers agreed that the installer is weakening the default Windows configuration and they remedied the issue.

When dealing with disparate system components, security assumptions are stacked on top of each other. Any room for ambiguity in these assumptions can lead to unintended and potentially exploitable behavior. We often see system components that enable insecure behavior downstream because their security assumptions are not guaranteed or enforced (e.g. making the assumption that no world writable folders exist in the system search path or that the access control permissions of one application do not affect other applications). While this can be somewhat mitigated by clear documentation that specifies how and when insecure behavior might be introduced, it is recommended where possible to explicitly prevent abuse of your API and feature sets. When developing library code or system features, you often can not predict how and where other system components will interact with your functionality. Security is a shared responsibility and proactively taking ownership of security problems helps the software community as a whole.