Get unstuck

Hint 1: A shell string is a program

Everything you paste into runner.exec is parsed by /bin/sh, where ; | & $( ) ` > and newline have meaning. runner.execFile takes the arguments as an array and starts git directly.

Hint 2: An argument can still be an option

git reads a value that starts with "-" as an option until it meets "--". Put "--" before the path, refuse a ref that starts with "-", and accept only the three view names.

Reveal the reference patch and explanation
Fixedgit-view.mjsJavaScript
const VIEWS = ['log', 'blame', 'diff'];

export async function runGit(request, runner) {
  const {view, ref, path} = request;
  if (!VIEWS.includes(view)) return {status: 400};
  if (typeof ref !== 'string' || ref === '' || ref.startsWith('-')) return {status: 400};
  if (typeof path !== 'string' || path === '') return {status: 400};
  // No shell: each value is one argument, and "--" ends git's options before the path.
  const result = await runner.execFile('git', [view, ref, '--', path]);
  return {status: 200, output: result.stdout};
}

log main -- README.md, blame v1.2.0 -- src/app.js and diff release/2.4 -- src/server.js run as git with four arguments through execFile. docs/release notes.md, docs/it’s-done.md, docs/say "hi".md, docs/café-menü.md and -notes.md each arrive as one path after "--". A path or ref containing ; | && $( ) backticks, a newline, a quote that would close single quotes, or > is passed to git as literal text, so no second command runs and no file is written. --output=public/log.txt and --contents=/etc/passwd as the ref, the checkout subcommand, the view --exec-path=/tmp and an empty path are refused with 400 before anything starts.

Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.

Inspect the actual test harness

The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.

Download cases.mjs
How the browser sandbox runs your code

Your code runs in QuickJS 2025-09-13 (quickjs-emscripten 0.32.0) inside a Web Worker, isolated from the page, with a 32 MiB memory limit and a 2.5-second time limit. The test cases and the pass or fail verdict run outside that sandbox, so changing built-ins such as Array.prototype.push cannot change the result. The lab download runs the same cases under Node.js, where your code runs directly.

The same as in current Node.js:

  • ES2025 syntax and built-ins, including Set methods such as intersection, iterator helpers, Promise.try, RegExp.escape and the RegExp v flag

Added to the sandbox so that it behaves like Node.js:

  • structuredClone, Array.fromAsync, queueMicrotask, atob and btoa
  • URL and URLSearchParams, parsed by your browser’s own URL implementation
  • TextEncoder and TextDecoder for UTF-8
  • Intl.Collator and a locale-aware String.prototype.localeCompare
  • console.log and related methods, whose output appears with your results

Not available in the browser sandbox:

  • Timers (setTimeout, setInterval), fetch and other network or storage APIs; the exercises are deterministic and offline
  • Node.js modules and globals: import, require, process and Buffer
  • Web Crypto (crypto.getRandomValues, crypto.subtle); each workshop supplies the cryptographic operations it needs
  • Intl other than Collator: toLocaleString and toLocaleDateString ignore the locale, so numbers have no digit grouping and dates use one fixed format in the device’s time zone
  • using declarations, Symbol.dispose and DisposableStack (explicit resource management)
  • Uint8Array.fromBase64 and toBase64, JSON.rawJSON, Atomics and Error.captureStackTrace

Use the lesson in your own system

The runner is a model: the grader parses command lines and git arguments rather than starting processes, and it covers the POSIX shell features the cases use, not every shell dialect. In a real service, also run git with least privilege against a read-only copy of the repository, set a timeout and an output limit, and keep stderr away from clients. Since Git 2.24, git also accepts --end-of-options, which ends option parsing in commands where "--" separates revisions from paths; refusing a leading dash works with any version. Other programs treat "--" differently: check each tool’s documentation before relying on it.

Your next task: Where does your code start another program, and does any of those calls pass a single command string, or let a request value appear before "--"?

OWASP OS Command Injection Defense Cheat Sheet

Related guide: Keep untrusted values out of executable syntax