Summary

A typical usage of Opener is to open a url such as https://google.com. Opener will launch a browser and open that page. It can also be used to execute commands such as npm run lint. Although code execution is part of the intended purpose of Opener, we believe it is a security issue if, for example, a crafted url can run an arbitrary shell command rather than just launching a browser.

On Windows, in contrast to other platforms such as MacOS and Linux, Opener uses a shell (cmd.exe) to open links. This creates a risk that an attacker can execute arbitrary shell commands.

Product

Opener

Tested Version

1.5.1

Details

Issue 1: Command injection on Windows

lib/opener.js has logic for three platforms: Linux, MacOS, and Windows. On Linux and MacOS, Opener uses xdg-open and open, respectively, to open the url. Those programs are specifically designed for opening links. On Windows, however, Opener uses cmd.exe to open the link. That means that special characters in the argument list could lead to the execution of arbitrary shell commands. The code already contains logic to handle the & character, but it does not handle the ^ character, which has a similar effect.

Below is example of how a client of the Opener library might unwittingly expose themselves to this vulnerability. The value of ${user} is attacker-controlled in this example, but you would not expect that to enable an attacker to run arbitrary shell commands.

const opener = require("opener");
opener(`https://github.com/${user}`);

If ${user} is the string ^&calc, then on Windows this code will open the (meaningless) url https://github.com/%5E in a browser and launch calc.exe.

Impact

On Windows, this issue may lead to remote code execution if a client of the library calls the vulnerable method with untrusted input. Other platforms (MacOS, Linux) are not vulnerable.

Remediation

We recommend adding logic to also escape ^ characters, as follows:

index 5fa88f3..08888c6 100644
--- a/lib/opener.js
+++ b/lib/opener.js
@@ -55,9 +55,9 @@ module.exports = function opener(args, options, callback) {
         // Furthermore, if "cmd /c" double-quoted the first parameter, then "start" will interpret it as a window title,
         // so we need to add a dummy empty-string window title: http://stackoverflow.com/a/154090/3191
         //
-        // Additionally, on Windows ampersand needs to be escaped when passed to "start"
+        // Additionally, on Windows ampersand and caret need to be escaped when passed to "start"
         args = args.map(function (value) {
-            return value.replace(/&/g, "^&");
+            return value.replace(/[&^]/g, "^$&");
         });
         args = ["/c", "start", "\"\""].concat(args);
     }

Resources

We have written a CodeQL query, which automatically detects this vulnerability.

Coordinated Disclosure Timeline

2020-08-26: Report sent to d@domenic.me 2020-08-26: Rejected as not-a-security-issue by d@domenic.me 2020-08-27: Posted PR to fix the issue: https://github.com/domenic/opener/pull/34

Credit

This issue was discovered by GitHub Engineer @max-schaefer (Max Schaefer).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2020-145 in any communication regarding this issue.