Skip to content

Fix potential OS command injection in jekyll-hook.js - #43

Open
DongZifan wants to merge 2 commits into
developmentseed:masterfrom
DongZifan:master
Open

Fix potential OS command injection in jekyll-hook.js#43
DongZifan wants to merge 2 commits into
developmentseed:masterfrom
DongZifan:master

Conversation

@DongZifan

Copy link
Copy Markdown

Command Injection Reproduction Notes for jekyll-hook.js

Summary

Hello,
I am writing to report a potential OS Command Injection vulnerability in the following file:
jekyll-hook/jekyll-hook.js

The issue occurs when user-controlled input from a GitHub Webhook POST request is passed directly as arguments to child_process.spawn() without any sanitization. The application extracts the repository name (data.repository.name), branch name (from data.ref), and owner name (data.repository.owner.name) from the webhook request body, pushes them into a params array, and passes them to the run() function which calls spawn(file, params).

On Windows, when file is a batch script (e.g., build.bat), Node.js implicitly invokes cmd.exe to execute it. Since cmd.exe interprets shell metacharacters (such as &, |, ") within arguments, an attacker who controls the repository name or other webhook fields can inject arbitrary commands that will be executed on the host system. This could potentially allow Remote Code Execution (RCE) on the server processing the webhook.

Root Cause

The issue is caused by unsanitized external input reaching child_process.spawn() in the following function:

function run(file, params, cb) {
    // When file is a .bat script on Windows, Node.js implicitly calls cmd.exe,
    // which parses shell metacharacters in params (e.g., &, |, ") as commands.
    var process = spawn(file, params);

    process.stdout.on('data', function (data) {
        console.log('' + data);
    });

    process.stderr.on('data', function (data) {
        console.warn('' + data);
    });

    process.on('exit', function (code) {
        if (typeof cb === 'function') cb(code !== 0);
    });
}

The params array is populated directly from the webhook request body without any input validation:

data.repo = data.repository.name;
data.branch = data.ref.replace('refs/heads/', '');
data.owner = data.repository.owner.name;

// ...

params.push(data.repo);
params.push(data.branch);
params.push(data.owner);

Reproduction Material

A minimal reproduction script is provided in: poc_jekyll_hook.js
poc_jekyll_hook.js

This Proof of Concept (PoC) is intended to demonstrate that external input can reach dangerous command execution logic through the vulnerable code path.

What the PoC Does

The PoC performs a minimal end-to-end trigger of the vulnerable code path:

  1. It hooks child_process.spawn() to intercept and inspect the arguments passed to the execution sink.
  2. It mocks the application dependencies (config.json, express, queue-async, emailjs) via a Module._load hook so that no real server or external service is needed.
  3. It loads the vulnerable jekyll-hook.js directly via require(), which starts the mocked HTTP server.
  4. It sends a crafted POST request to /hooks/jekyll//master with:
    • A valid X-Hub-Signature header (computed with the mocked secret)
    • A repository.name value containing shell metacharacters: my_repo" & calc #
  5. This causes the application to extract data.repo = 'my_repo" & calc #' from the request body.
  6. The malicious repository name is pushed into the params array and passed to spawn(file, params) in the run() function.
  7. The hooked spawn() detects the & calc substring in the arguments, confirming the injection.

In the provided example, the injected payload is crafted so that, on Windows, successful command execution opens the Calculator application. This serves as a visible indicator that external input can reach the OS command execution sink without proper sanitization.

Example Payload

The PoC uses the following payload for repository.name in the Webhook JSON body:

{
  "ref": "refs/heads/master",
  "repository": {
    "name": "my_repo\" & calc #",
    "owner": {
      "name": "attacker_user"
    }
  },
  "pusher": {
    "email": "test@example.com"
  }
}

How to Run

Run from the 939 directory:

node poc_jekyll_hook.js

Expected Output

When the PoC runs, you should see output similar to:

Listening on port 8080
[POC] sending webhook payload to port 8080

[POC] spawn would run:
 build.bat [ 'my_repo" & calc #', 'master', 'attacker_user', ... ]

[POC SUCCESS] Command injection confirmed in spawn arguments!
[POC] status = 202

In the vulnerable version, after the crafted request is processed, the local machine will launch the Calculator application as a benign demonstration effect.
This shows that the attacker-controlled repository.name value can influence command execution behavior through the vulnerable run() path.

Patch Explanation

This branch also includes a patched version of jekyll-hook.js (new_jekyll-hook.js) intended to mitigate the command injection risk described above.

What the patch changes

The patch adds strict validation before webhook data values are used by the processing logic.

In the vulnerable version, user-controlled values such as:

  • data.repository.name
  • data.ref (used to derive the branch name)
  • data.repository.owner.name

could reach child_process.spawn(...) through the params array without any validation. Additionally, the HMAC-SHA1 signature verification could be silently bypassed by omitting the secret from the configuration — the original code simply returned early instead of rejecting the request.

The patched version introduces input checks before these values are passed into command execution logic, and makes HMAC signature verification mandatory.

Validation introduced by the patch

The patched version ensures that all external inputs are verified against a strict whitelist before they are used:

  • Character whitelist: All critical parameters (repo, branch, owner) must match the regular expression /^[a-zA-Z0-9._-]+$/.
  • Allowed characters: Only alphanumeric characters, hyphens (-), underscores (_), and dots (.) are permitted.
  • Rejection of metacharacters: Any input containing shell metacharacters (e.g., &, |, ;, $, >, <) or double quotes is rejected immediately, and a security error is logged.
  • Mandatory HMAC verification: If no config.secret is configured, the server now throws a 500 error instead of silently skipping signature verification. Requests missing the X-Hub-Signature header or containing an improperly formatted signature are rejected with a 403 status.

By performing these checks at the application layer, the patch effectively prevents command injection through shell metacharacter interpretation when spawn() interacts with cmd.exe on Windows.

Add validation for repository, branch, and owner names to prevent invalid characters in webhook data.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant