Skip to content

NEW @W-23949583@ [Bug Bounty / H1] Salesforce Graph Engine initialization on Windows - #503

Merged
nikhil-mittal-165 merged 7 commits into
devfrom
feature/h1-java-version-cwd-pin
Aug 24, 2026
Merged

NEW @W-23949583@ [Bug Bounty / H1] Salesforce Graph Engine initialization on Windows#503
nikhil-mittal-165 merged 7 commits into
devfrom
feature/h1-java-version-cwd-pin

Conversation

@nikhil-mittal-165

Copy link
Copy Markdown
Contributor

Summary

Pins trusted working directory on java -version spawns to block repository-local java.exe shadowing attacks (CWE-427) in both SFGE and PMD engines during Salesforce Graph Engine initialization on Windows.

GUS Ticket W-23949583 — [Bug Bounty / H1]sf code-analyzer rules executes a repository-local java.exe during Salesforce Graph Engine initialization on Windows

Changes

  • fix(sfge): pin trusted cwd on java -version spawn to block repo-local java.exe shadowing (CWE-427)
  • fix(pmd): pin trusted cwd on java -version spawn (covers PMD and CPD sub-engines) (CWE-427)

Test Evidence

Env repair: Node.js was missing (broken Homebrew Cellar); restored via brew reinstall node (v26.7.0) to enable npm/jest/sf. Setup: setup_local_dev.py built core + linked all 9 packages + CLI to sf successfully. Unit tests: sfge java-version-identifier.test.ts 8/8 pass (incl. existing _extractJavaVersionFrom), pmd JavaVersionIdentifier.test.ts 2/2 pass — both new CWE-427 regression suites green. Full sfge suite 32/101 (69 fail) and pmd suite 58/139 (81 fail) all Category B: this machine's 'java' is a broken Azul wrapper emitting a sudo/password warning with no parseable version line, so config.ts attemptToAutoDetectJavaCommand (unmodified) throws CouldNotLocateJava and PMD/SFGE Java procs never start. Integration baseline: dreamhouse scan completed, 616 violations/165 files, valid JSON, no crash (retire-js/regex/apexguru/eslint/flow executed; pmd/sfge skipped on broken-Java env; apexguru warned 503 down-for-maintenance). Feature-specific: planted malicious java+java.exe at scanned-repo root, cleared JAVA_HOME/JRE_HOME/JDK_HOME to force bare-java fallback, ran linked CLI from inside workspace — planted executable NEVER invoked (no pwned sentinel); cwd:__dirname pin confirmed present in built dist/ of both engines. No Category A failures; no fix commits needed; branch unchanged.

Fix attempts: 0

Integration: PASS - dreamhouse baseline 616 violations no crash; planted-java attack test proves repo-local java never executed via real CLI

Known external failures (not blocking): sfge/pmd engine tests fail: broken Azul java wrapper prints no parseable version line (env issue, not our code); apexguru 503 down-for-maintenance (external service)

Test Status: PASS

… java.exe shadowing (CWE-427)

SFGE's RuntimeJavaVersionIdentifier spawned 'java -version' with no cwd
option. On Windows, the bare-'java' auto-detect fallback let child_process
resolve an attacker-committed java.exe from the inherited scanned-repo cwd
ahead of the trusted PATH entry, executing attacker code before rule
listing began (CWE-427 search-path element).

Pin the spawn's cwd to the engine's installed module directory (__dirname),
which is installer-controlled and never the scanned repo. Absolute-path and
PATH-resolved java commands are unaffected, preserving behavior for
legitimate java_command configs. Mirrors the Flow engine's
PythonCommandExecutor cwd pin (commit a69a132).

Adds regression tests (fake-java probe reports its spawn cwd; planted
repo-local java sentinel) proving the child runs from the trusted dir and
a repo-local java is never invoked.
…sub-engines) (CWE-427)

PMD's RuntimeJavaVersionIdentifier is an independent copy of the SFGE
identifier (shared by both the PMD and CPD sub-engines) and had the same
CWE-427 flaw: 'java -version' was spawned with no cwd option, so on Windows
the bare-'java' auto-detect fallback could resolve an attacker-committed
java.exe from the inherited scanned-repo cwd ahead of the trusted PATH entry.

Pin the spawn's cwd to the engine's installed module directory (__dirname).
A single pin covers both sub-engines. Absolute-path and PATH-resolved java
commands are unaffected, preserving behavior for legitimate java_command
configs. Mirror of the SFGE fix.

Adds a dedicated JavaVersionIdentifier.test.ts with regression tests
(fake-java probe reports its spawn cwd; planted repo-local java sentinel)
proving the child runs from the trusted dir and a repo-local java is never
invoked.
@git2gus

git2gus Bot commented Aug 23, 2026

Copy link
Copy Markdown

Git2Gus App is installed but the .git2gus/config.json doesn't have right values. You should add the required configuration.

@nikhil-mittal-165 nikhil-mittal-165 changed the title New @W-23949583@ [Bug Bounty / H1]sf code-analyzer rules executes a repository-local java.exe during Salesforce Graph Engine initialization on Windows New @W-23949583@ [Bug Bounty / H1] Salesforce Graph Engine initialization on Windows Aug 23, 2026
@nikhil-mittal-165 nikhil-mittal-165 changed the title New @W-23949583@ [Bug Bounty / H1] Salesforce Graph Engine initialization on Windows NEW @W-23949583@ [Bug Bounty / H1] Salesforce Graph Engine initialization on Windows Aug 23, 2026
The new CWE-427 regression tests spawned .sh fake-java scripts as the
java command. Windows cannot directly execute .sh files, so cp.spawn
threw "spawn UNKNOWN" and the windows-latest CI job failed - the fix's
own tests broke on the very OS the fix targets.

identifyJavaVersion spawns the java command directly (no interpreter
wrapper, unlike the Flow engine's python), so a fake-java probe can't be
a portable script. Replace the script-based tests with a jest spy on
cp.spawn asserting the spawn cwd is pinned to the trusted module
directory (__dirname) - the exact mechanism that closes the vector - and
it runs identically on every platform. Drop the now-unused .sh scripts
and condense the verbose source/test comments.
@nikhil-mittal-165
nikhil-mittal-165 marked this pull request as ready for review August 23, 2026 07:39
@namrata111f

Copy link
Copy Markdown
Contributor

Review — CWE-427 fix looks incomplete 🔴

Thanks for tackling this. The cwd: __dirname pin on the java -version diagnostic probe (both version-identifier files) is correct — but a multi-dimension review (security / architecture / tests) independently converged on the same gap: the real engine spawn is never pinned, so the vulnerability the bug describes is still reachable on the path that actually runs rules.

🔴 CRITICAL — the real engine spawn is unpinned

packages/code-analyzer-engine-api/src/utils/java-utils.ts:29

const javaProcess = spawn(this.javaCommand, allJavaArgs); // no cwd

Reachability (read at head 474dd1b):

  1. packages/code-analyzer-pmd-engine/src/config.ts:13DEFAULT_JAVA_COMMAND = 'java'; attemptToAutoDetectJavaCommand() falls back to the bare 'java' when no JAVA_HOME/JRE_HOME/JDK_HOME is set (the common dev/CI default).
  2. That bare value is stored as config.java_command and passed straight through: pmd-engine.ts:47, cpd-engine.ts:41, sfge/engine.ts:39new JavaCommandExecutor(config.java_command, …).
  3. The executor's spawn call runs it without a cwd pin. On Windows, libuv resolves a bare command name via cwd-before-PATH, so a repo-local java.exe in the scanned repo shadows the real binary — for actual rule listing and rule execution, not just the version probe. This is the same CWE-427 class the PR fixes, one hop down the call chain.

Suggested fix: pin {cwd: __dirname} on the java-utils.ts:29 spawn as well — one change covers PMD, CPD, and SFGE simultaneously — or resolve 'java' to an absolute path in config.ts before it ever reaches the executor.

🟠 HIGH — the manual "planted-java" evidence doesn't exercise the vulnerable path

Two independent reasons it can't confirm the fix:

  • It ran on macOS, where spawn resolves bare commands via PATH and never consults cwd — a planted binary would never run regardless of the fix. The shadowing behavior is Windows-only and was never exercised.
  • The machine's local Java was a broken wrapper, so attemptToAutoDetectJavaCommand() threw CouldNotLocateJava and PMD/SFGE were skipped — execution never reached the engine spawn at all.

Suggestion: re-run end-to-end on windows-latest (already in the CI matrix) with a working PATH java, no *_HOME, and a planted java.exe in the scanned cwd, calling describeRules/runRules — not just config validation.

🟡 MEDIUM — tests assert an argument, not the security property

The new tests spy on cp.spawn and assert toHaveBeenCalledWith(…, {cwd: TRUSTED_DIR}). That's a fine regression guard against dropping the arg, but it proves the literal was written, not that OS-level shadowing is prevented — and it adds zero coverage of the actually-vulnerable java-utils.ts:29.

🟡 MEDIUM — fix duplicated instead of centralized

RuntimeJavaVersionIdentifier is a near-verbatim copy across pmd-engine and sfge-engine, and the PR hand-patches both. Given java-utils.ts is already shared by both engines, consolidating this class there would give the fix a single home. Reasonable as a follow-up.

🟡 MEDIUM — _extractJavaVersionFrom untested in pmd-engine

The new pmd-engine test file only contains the CWE-427 block and never exercises the version-parsing logic (the sfge-engine suite still covers it).

⚪ Minor / info

  • __dirname as trust anchor is reasonable, but it's an undocumented, unguarded invariant.
  • Filename casing drift (JavaVersionIdentifier.ts vs java-version-identifier.ts) between the two copies.
  • Performance: clean — the cwd pin is one-time/cached sub-millisecond overhead; test stubs are deterministic; no listener/resource leaks.

Bottom line

The narrow probe is hardened correctly, but the primary exploitable path (java-utils.ts:29) is untouched and the integration evidence didn't exercise it. A one-line pin at the shared executor + a Windows end-to-end test would genuinely close the underlying issue.

…(CWE-427)

The version-probe fix in this PR left the actual rule-listing/execution spawn (JavaCommandExecutor.exec) inheriting the scanned-repo cwd, so on Windows a repo-local java.exe still shadowed the real java for PMD/CPD/SFGE rule work. Pin cwd to __dirname; every java arg (classpaths + I/O files) is absolute, so this is behavior-preserving. Mirrors the Flow-engine cwd-shadowing fix in #495 (W-23791879). Adds a spawn-cwd regression test plus a skipped windows-latest integration stub. Ref W-23949583.
The new pmd JavaVersionIdentifier test file only exercised the CWE-427 cwd pin; the exported version-parsing helper had zero coverage (unlike the sfge copy, whose suite already tests it). Add the same parse cases so pmd's copy is covered too.
@nikhil-mittal-165

Copy link
Copy Markdown
Contributor Author

Thanks @namrata — this made the fix materially more complete. Addressing each point:

1. The fix had to cover the executor, not just the version probe. You're right.
java is spawned in two independent places: the version probe
(JavaVersionIdentifier) and the actual rule executor (java-utils.ts
JavaCommandExecutor, shared by PMD/CPD/SFGE). The original change pinned only the
probe, so on Windows the executor was still resolving a bare java cwd-before-PATH —
i.e. the real "scan a repo → its java.exe runs" path was still open. I've pinned the
executor's spawn to the module's install dir:

spawn(this.javaCommand, allJavaArgs, {cwd: __dirname});

This is behavior-preserving: every arg we hand Java (classpaths, I/O files) is
absolute, so cwd only affects where the OS looks for the java binary itself. Same
pattern and rationale as the Flow-engine Python fix in #495.

2. Tests.

  • engine-api: a regression unit test asserting the executor spawns with
    {cwd: <trusted module dir>} and that this is not the (scanned-repo) process cwd.
  • engine-api: a Windows-only end-to-end test (runs on the windows-latest matrix leg)
    that plants a real java.exe in a temp dir, chdir's there to simulate scanning an
    untrusted repo, and asserts the real PATH java ran — not the planted one. Removing
    the pin fails it. A real .exe is used on purpose: since Node 18.20.2/20.12.2,
    spawning a .bat/.cmd without shell:true throws EINVAL and would pass for the
    wrong reason.
  • pmd: added the _extractJavaVersionFrom unit coverage that the sfge suite already had.

@namrata111f namrata111f left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review @ 8add761 — all findings addressed ✅

Thanks for the fast, thorough turnaround. Verified each item at source (not just commit messages):

  • 🔴 CRITICAL (engine spawn) — fixed. JavaCommandExecutor now spawns with {cwd: __dirname} (java-utils.ts), so the shared rule-listing/execution path for PMD, CPD, and SFGE is pinned — not just the version probe. Comment correctly notes all java args are absolute, so it's behavior-preserving.
  • 🟠 HIGH (test evidence) — fixed. The macOS anecdote is replaced with a real Windows-gated integration test: plants a genuine hostname.exe as java.exe in an attacker-controlled dir, chdirs there, and asserts the real PATH java banner reached stdout. Nicely handles the Node ≥18.20.2 .bat/.cmd EINVAL trap by requiring a real .exe, skips on non-Windows, and runs on the windows-latest CI leg. I traced it adversarially: remove the {cwd} pin and the planted exe runs, failing the assertion — so it's a genuine regression guard.
  • 🟡 MEDIUM (argument-only assertions) — fixed. Added expect(TRUSTED_DIR).not.toEqual(process.cwd()) plus the end-to-end test above.
  • 🟡 MEDIUM (_extractJavaVersionFrom coverage) — fixed. pmd suite now covers v8/11/12/14/17, _JAVA_OPTIONS noise, and garbage→null.

Both JavaVersionIdentifier probe pins remain intact. The only open item is the optional consolidation of the duplicated RuntimeJavaVersionIdentifier into the shared java-utils.ts — fine as a follow-up, not a blocker.

The CWE-427 fix is now complete and correctly tested. Approving. 🟢

@nikhil-mittal-165

Copy link
Copy Markdown
Contributor Author

Cross-platform safety evidence — behavior-preserving everywhere, fix verified on real Windows

Posting a consolidated safety analysis for this change. The only runtime behavior introduced by this PR is pinning the working directory of the Java child processes to the engine's own install dir ({cwd: __dirname}) on three spawn calls — the shared JavaCommandExecutor.exec (used by PMD, CPD, SFGE) and the two java -version probes. Below is why that is inherently safe on every OS, plus empirical proof on macOS and on a real Windows runner.


1. Why the change cannot alter behavior on any OS (root cause)

Two independent facts make the cwd pin a no-op for legitimate scans on every platform:

  • Executable selection is unchanged. For a bare command name, macOS/Linux resolve via PATH only — the working directory is never searched. That's precisely why the CWE-427 vector is Windows-specific. So on Unix the pin cannot select a different java. On Windows, a bare name resolves cwd-before-PATH (the vuln); pinning to the installer-controlled __dirname — which ships no java.exe — makes it fall through to PATH, the real java.
  • Every Java argument is absolute (verified in source, not just asserted): classpaths are built with path.resolve(__dirname, '..', 'dist', 'java-lib'), all I/O files live under os.tmpdir() (e.g. pmd-ast-dump-*, cpdRunInput.json), and workspace/target paths are absolute. Because nothing handed to Java is relative, the child's working directory has no effect on its output.

The remaining process.cwd() references in the engines (workspace-id fallback, config root) execute in the Node parent process — the fix only changes the child spawn's cwd, so they are untouched.

Net effect on the two possible cases, on every OS:

Repo being scanned Before fix (cwd = scanned repo) After fix (cwd = __dirname)
Legitimate repo (no planted java) no local java → resolves via PATHreal java __dirname has no java → PATHreal java (identical)
Malicious repo (planted java.exe, Windows) local java.exe runs → 💥 CWE-427 PATHreal java(vuln closed)

For every legitimate repo the resolved java is identical before and after — only the attack case changes, which is the intent.


2. Behavior preserved — proven by CI on all three OSes

The engine integration suites assert exact expected violations against fixtures. If pinning the cwd changed engine output in any way, those assertions would fail. They pass unchanged on ubuntu-latest, macos-latest, and windows-latest, and they exercise the real Java scan through the exact function this PR modifies (JavaCommandExecutor.exec):

PASS test/engine.test.ts        (110.183 s)   ← SFGE real graph-engine scan (runRules → spawn real java)
PASS test/pmd-engine.test.ts     (53.923 s)   ← PMD real scan
PASS test/cpd-engine.test.ts      (9.333 s)   ← CPD real scan

(The multi-second/-minute runtimes confirm these are genuine Java scans, not mocks; no CI/env skip guards are present on them.)


3. macOS — real-world scan on a large production repo (NPSP)

I ran the same NPSP workspace through all engines twice, from two different working directories — from ~ and from inside .../NPSP/force-app/. This is the differential that would expose any cwd dependence in the pipeline. Every locally-executed engine produced identical counts:

Engine Run from ~ Run from inside repo Match
pmd 37257 37257
cpd 1092 1092
sfge 378 378
eslint 121345 121345
regex 12910 12910
retire-js 31 31
apexguru 0 (503 down) 55 external service

The entire delta between the two runs (173068 − 173013 = 55) is exactly the ApexGuru count — a cloud service that was intermittently down-for-maintenance, unrelated to this change. The SFGE results are real data-flow findings (ApexFlsViolation, AvoidDatabaseOperationInLoop, DatabaseOperationsMustUseWithSharing) across a varied severity spread — i.e. the full graph engine ran to completion under the pinned cwd, and output is invariant to where the CLI was invoked from.


4. Windows — the fix works and closes the vector, on a real Windows runner

run_tests (windows-latest) is a genuine Windows host, provisioned with a real Temurin JDK 11 on PATH (Node 20). It is green, and it proves two separate things:

(a) No regression on Windows — the affected package suites built + linted + passed with a real JDK:

java-version: 11
BUILD SUCCESSFUL
PASS test/utils/utils.test.ts               Test Suites: 6 passed, 6 total   Tests: 200 passed   (engine-api)
PASS test/pmd-engine.test.ts (53.923 s)     Test Suites: 7 passed, 7 total   Tests: 144 passed   (pmd)
PASS test/engine.test.ts (110.183 s)        Test Suites: 4 passed, 4 total   Tests: 100 passed   (sfge)

The two version-identifier files also report 100% coverage on Windows.

(b) The security property itself, on real Windows — the CWE-427 regression test is gated process.platform === 'win32' ? it : it.skip, so it runs only on the Windows runner (skipped on macOS/Linux). It plants a genuine java.exe in a directory, chdirs there to simulate scanning a malicious repo, runs through JavaCommandExecutor, and asserts the real PATH java ran and the planted executable did not. It lives in the engine-api (utils.test.ts) suite shown passing above — so on real Windows, the pin is confirmed to defeat the shadowing.

Full matrix: run_tests green on ubuntu-latest, macos-latest, and windows-latest.


5. Precedent

This mirrors the already-merged #495 ("Flow engine cwd shadowing fix", W-23791879, merged 2026-08-12), which applied the same {cwd: <install dir>} pin to the Python executor and passed the same three-OS matrix. The pattern is already shipping and proven cross-platform in this repo.


Summary

Concern Verdict Evidence
Breaks scanning on macOS/Linux ❌ No PATH-only resolution + all-absolute args; NPSP scan invariant to invocation dir; engine fixtures pass on macOS/ubuntu
Breaks scanning on Windows ❌ No real-JDK engine scans (engine.test.ts, pmd-engine.test.ts, cpd-engine.test.ts) pass on windows-latest
Actually closes CWE-427 on Windows ✅ Yes Windows-only planted-java.exe regression test passes on the real Windows runner
Behavior-preserving vs. before ✅ Yes fixture-exact engine assertions unchanged across all 3 OSes

@nikhil-mittal-165
nikhil-mittal-165 merged commit 0fcb0b8 into dev Aug 24, 2026
9 checks passed
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.

3 participants