A Privilege Escalation Case Study

Security boundaries get blurry when one software component quietly breaks the assumptions another component relies on. The local privilege escalation vulnerability tracked as CVE-2020-15145 in the Windows installer for Composer, the PHP package manager, is a textbook example. The fix shipped in version 6.0.0, but the underlying question — who owns the remediation when the bug spans an application, an installer, and the operating system — remains open.

Composer's Windows installer placed its executables and libraries in C:\ProgramData\ComposerSetup\bin and added that directory to the system PATH. The problem was the folder's permissions: any local user could create or modify files there. That opened two distinct attack paths:

  1. A regular user could alter the existing composer.bat script. When an administrator later ran Composer, the modified batch file would execute arbitrary commands with elevated rights.
  2. A user could plant a specially crafted DLL in the Bin folder. Because that folder sat in the system PATH, any process using dynamic linking would search it when a DLL wasn't found elsewhere — a classic DLL planting scenario that could escalate to Local System privileges.

The "regular user" here is shorthand for anyone with write access to the folder. In practice, it could be any malicious process looking to persist with higher privileges when an unsuspecting user runs an application.

How PATH Misalignment Happens

The PATH environment variable serves two very different communities. For end users, it is a convenience: a list of directories searched for executables when a command is typed without a full path, either in a user-specific or system-wide scope.

For developers, PATH plays a role in run-time dynamic linking. When code calls LoadLibrary or LoadLibraryEx with a bare DLL name, Windows walks its search order and finally checks the directories listed in PATH. This linking style is sometimes intentional — for instance, when a DLL might not exist on all Windows versions. But it means that any writable directory on the PATH is a potential injection point: put a malicious DLL with the right filename there, and a privileged application that fails to resolve a dependency will silently load your code.

Microsoft's documentation for loading libraries safely gives developers ways to restrict the search order, yet DLL planting vulnerabilities that escalate to Local System remain surprisingly common.

Allocating Responsibility

At first glance, both sides of the Composer issue share blame. The installer introduced the weakness by putting an insecure folder on the system PATH. Windows, however, does not treat PATH entries as writable by everyone; Microsoft has explicitly stated that system PATH locations are not supposed to be world writable. One could imagine Windows forcing all privileged services to load only from base system directories, but at that scale any enforcement risks breaking third-party applications that rely on current behavior. System-wide mitigations quickly become a question of regression risk.

The practical conclusion is that security remains a shared contract. Applications should avoid introducing weaknesses into the operation environment they run in, and the platform layer should reduce the blast radius when an application fails. Crucially, application developers should verify that their code still behaves safely even if platform-level mitigations are bypassed or not applied.

Without that dual ownership, remediation can stall entirely. Composer's authors might argue that local users are trusted and lenient folder permissions are required for frictionless package management; developers of privileged tools might argue they reasonably assume everything on the system PATH is trusted and secured. In this case, the Composer team accepted that their installer weakened the default Windows configuration and fixed the problem.

Do Not Stack Assumptions Blindly

When distinct system components interact, security assumptions stack. Ambiguity anywhere in that stack leads to behavior that no single party designed or intended. A component often enables downstream insecurity not on its own, but by assuming something it never enforces — that no writable directories appear in the system search path, or that one application's access controls cannot affect another application.

Clear documentation can shrink that ambiguity by spelling out when and how a feature might introduce insecure conditions. But documentation is a fallback. Where code and design allow, the more reliable approach is to explicitly prevent abuse of an API or feature set, because library authors and system designers cannot anticipate every context in which their functionality will be used. Taking ownership of those failure modes is part of the shared security workload.