Skip to content
winfunc
Disclosure record · Hoppscotch

Hoppscotch CLI sandbox escape through Node vm pre-request scripts (CVE-2024-34347)

Hoppscotch CLI ran collection scripts in Node's vm while exposing host-created objects

HoppscotchCVE-2024-34347DisclosedSource record

CVSS 3.1 base score

High
8.3/ 10

Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H

Record

Project
Hoppscotch
Severity
High8.3
CVE
CVE-2024-34347
Disclosed
Trace
6 steps

Summary

Source

test

packages/hoppscotch-cli/src/commands/test.ts:14

Sink

runInContext

packages/hoppscotch-js-sandbox/src/pre-request/node-vm/index.ts:31

Hoppscotch CLI executes collection-supplied pre-request and test scripts while running hopp test. Unlike the web and desktop clients, the CLI could not rely on browser Web Workers and used the Node.js vm module through @hoppscotch/js-sandbox. The sandbox created a vm context, then injected host-created objects such as pw, atob, and btoa into that context before calling runInContext on attacker-controlled collection script text.

Node's vm module is not a security boundary for untrusted JavaScript, especially when code inside the context can reach references to objects created outside the context. A malicious collection script could use the exposed pw object to recover the host global object and load child_process, gaining command execution on the machine running Hoppscotch CLI. The GitHub advisory is GHSA-qmmm-73r2-f8xr / CVE-2024-34347. The fix, merged in PR #3973 as commit 22c6eabd133195d22874250a5ae40cb26b851b01, removed the Node vm implementation and replaced it with an isolated-vm based Node sandbox. The patched npm release for @hoppscotch/cli is 0.8.0.

Severity

Attack vectorAV
Network
Attack complexityAC
High
Privileges requiredPR
None
User interactionUI
Required
ScopeS
Changed
ConfidentialityC
High
IntegrityI
High
AvailabilityA
High

Metric values as published in the disclosure vector. Meters show how far each value raises exposure.

Source-to-sink trace

  1. Source · attacker-controlledpackages/hoppscotch-cli/src/commands/test.ts:14

    test

  2. Step 01packages/hoppscotch-cli/src/commands/test.ts:14-20

    The CLI test command parses an attacker-supplied collection file and sends its requests through the collection runner.

    typescript
    export const test = (path: string, options: TestCmdOptions) => async () => {
      const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] }
      const collections = await parseCollectionData(path)
    
      const report = await collectionsRunner({ collections, envs, delay })
    }
    
  3. Step 02packages/hoppscotch-cli/src/utils/pre-request.ts:36-47

    For every request, the CLI passed the collection-controlled preRequestScript string into runPreRequestScript from @hoppscotch/js-sandbox/node.

    typescript
    export const preRequestScriptRunner = (request: HoppRESTRequest, envs: HoppEnvs) =>
      pipe(
        TE.of(request),
        TE.chain(({ preRequestScript }) =>
          runPreRequestScript(preRequestScript, envs)
        )
      )
    
  4. Step 03packages/hoppscotch-js-sandbox/src/pre-request/node-vm/index.ts:13-31 (before fix)

    The vulnerable Node implementation created a vm context, injected host-created pw, atob, and btoa references, then executed the untrusted script text in that context.

    typescript
    return createContext()
    
    const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
    
    context.pw = pw
    context.atob = atob
    context.btoa = btoa
    
    runInContext(preRequestScript, context)
    
  5. Step 04packages/hoppscotch-js-sandbox/src/test-runner/node-vm/index.ts:42-50 (before fix)

    Test scripts had the same trust-boundary break: the host-created pw object plus response data were exposed directly to untrusted testScript code before runInContext.

    typescript
    const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs)
    
    context.pw = { ...pw, response: responseObjHandle.right }
    context.atob = atob
    context.btoa = btoa
    
    runInContext(testScript, context)
    
  6. Step 05packages/hoppscotch-js-sandbox/src/node/pre-request.ts:20-70 (fix)

    The patched implementation creates an isolated-vm isolate and context, copies serialized API methods into the isolate, builds an in-isolate proxy, and runs the script inside that isolate.

    typescript
    const isolate: ivmT.Isolate = new ivm.Isolate()
    const context = await isolate.createContext()
    const jail = context.global
    
    const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
    const serializedAPIMethods = getSerializedAPIMethods(pw)
    jail.setSync("serializedAPIMethods", serializedAPIMethods, { copy: true })
    
    const finalScript = `
      const pw = new Proxy(serializedAPIMethods, {
        get: (pwObjTarget, pwObjProp) => {
          const topLevelEntry = pwObjTarget[pwObjProp]
    
          // "pw.env" set of API methods
          if (topLevelEntry && typeof topLevelEntry === "object") {
            return new Proxy(topLevelEntry, {
              get: (subTarget, subProp) => {
                const subLevelProperty = subTarget[subProp]
                if (subLevelProperty && subLevelProperty.typeof === "function") {
                  return (...args) => subLevelProperty.applySync(null, args)
                }
              },
            })
          }
        }
      })
    
      ${preRequestScript}
    `
    
    const script = await isolate.compileScript(finalScript)
    await script.run(context)
    
  7. Step 06packages/hoppscotch-js-sandbox/src/node/utils.ts:6-23

    Host API functions are converted to isolated-vm references recursively instead of passing a raw host object graph into a Node vm context.

    typescript
    export const getSerializedAPIMethods = (
      namespaceObj: Record<string, unknown>
    ): Record<string, unknown> => {
      const result: Record<string, unknown> = {}
    
      for (const [key, value] of Object.entries(namespaceObj)) {
        if (typeof value === "object" && value !== null && !Array.isArray(value)) {
          result[key] = getSerializedAPIMethods(value as Record<string, unknown>)
        } else if (typeof value === "function") {
          result[key] = new ivm.Reference(value)
        } else {
          result[key] = value
        }
      }
    
      return result
    }
    
  8. Sinkpackages/hoppscotch-js-sandbox/src/pre-request/node-vm/index.ts:31

    runInContext

Impact

Reported impact

Successful exploitation gives arbitrary command execution on the victim's machine. The attacker can read or modify local files, exfiltrate secrets available to the CLI process, run additional payloads, and pivot through any credentials or network access available to that user.

Attack surface

Hoppscotch CLI users who run hopp test on untrusted or attacker-supplied Hoppscotch collection exports containing pre-request or test scripts.

Preconditions

The attacker must convince a victim to download or otherwise use a malicious Hoppscotch collection and run it with an affected @hoppscotch/cli version from 0.5.0 before 0.8.0. Hoppscotch Web and Desktop are not affected by this Node-specific sandbox path.

Attack path

  1. 1

    Create a Hoppscotch collection with a malicious pre-request script or test script.

  2. 2

    Use the exposed pw host object reference to escape the Node vm context.

  3. 3

    Resolve the host global object and import child_process.

  4. 4

    Execute arbitrary commands with the privileges of the user running Hoppscotch CLI.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Install an affected @hoppscotch/cli release before 0.8.0, then prepare a Hoppscotch collection containing a request with a pre-request script.

  2. 02

    Target configuration

    The vulnerable path is the Node CLI sandbox. The web and desktop clients use the browser worker sandbox and are not affected by this specific Node vm escape.

  3. 03

    Exploit delivery

    Place this script in the collection's pre-request script field:

    js
    outside = pw.constructor.constructor('return this')()
    outside.process.mainModule.require('child_process').execSync('id > /tmp/pwnd')
    

    Then have the victim run the collection with Hoppscotch CLI.

  4. 04

    Expected response

    On vulnerable CLI versions, the command executes and writes the result of id to /tmp/pwnd. On fixed versions, the Node vm implementation is gone and the script runs in the isolated-vm based implementation.

  5. 05

    Outcome

    A collection-supplied script escapes the intended sandbox and executes operating-system commands as the CLI user.

Remediation

Guidance

Upgrade @hoppscotch/cli to 0.8.0 or later. Do not use Node's vm module as a security sandbox for untrusted collection scripts. If scripting cannot be isolated safely, disable scripting by default or require an explicit opt-in for untrusted CLI runs.

Before and after−11+16
import { createContext, runInContext } from "vm" const context = createContext()const { pw, updatedEnvs } = getPreRequestScriptMethods(envs) context.pw = pwcontext.atob = atobcontext.btoa = btoa runInContext(preRequestScript, context)resolve(updatedEnvs)import { createRequire } from "module"import type ivmT from "isolated-vm" const nodeRequire = createRequire(import.meta.url)const ivm = nodeRequire("isolated-vm") const isolate: ivmT.Isolate = new ivm.Isolate()const context = await isolate.createContext()const jail = context.global const serializedAPIMethods = getSerializedAPIMethods(pw)jail.setSync("serializedAPIMethods", serializedAPIMethods, { copy: true }) const script = await isolate.compileScript(finalScript)await script.run(context)await isolate.dispose()
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

Investigate the paths that matter in your codebase.

Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.