Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
fix: run the binary directly in verify_binary instead of through a shell (CWE-78) #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
fix: run the binary directly in verify_binary instead of through a shell (CWE-78) #43
Changes from all commits
db623c3ab48704e766c21File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
There are no files selected for viewing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[blocking] The fix on this line is correct — please don't change it. What's missing is that the repo's own security gate is red on it, and the written record says otherwise.
semgrep/cifails on this PR — exit code 1,Findings: 1 (1 blocking)(job log) — on this exact line:ruby.lang.security.dangerous-exec.dangerous-exec, raised as code-scanning alert #13 and as thegithub-advanced-security[bot]comment on this line. The rule flags any non-static first argument toIO.popenand does not model the array form, so post-fix it is a false positive: with no/bin/shin the picture there is nothing left to inject into.I verified the fix works by execution rather than by reading — reverting only
lib/browserstack/localbinary.rbtomastermakes both new tests fail (and printssh: /nonexistent: No such file or directory, proving the shell was involved pre-fix); with the fix, both pass.Two consequences worth resolving before a human is asked to merge:
mergeable_state: blocked), and the Jira record closes with "please review & merge PR fix: run the binary directly in verify_binary instead of through a shell (CWE-78) #43" without mentioning it. Its Testing section names Semgrep as one of the two live workflows on this repo, but never reports Semgrep's result — so the reviewer is told the change is ready while a security gate on the changed line is red.masteras alert updated gemspec #9 atlocalbinary.rb:136. This is pre-existing and merely re-anchored by the two added comment lines, not introduced here.Suggested resolution — no code change, a rule-scoped suppression carrying the justification, which clears both this PR's check and the standing
masteralert:Alternatively, triage alerts #13 and #9 as false-positive in the Semgrep platform if that is the team's convention — the repo has no existing
nosemgrepprecedent either way, so this is a judgment call. Whichever route, please record the outcome in the PR description and correct the Jira comment, so the next reader isn't left reconciling a green write-up against a red check.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accepted in full — resolved in
ab48704. I verified every claim independently before acting rather than taking the finding on trust:semgrep/ciFAILURE on job 94071567985 (Findings: 1 (1 blocking),Has findings for blocking rules so exiting with code 1), alert #13 atlocalbinary.rb:138onrefs/pull/43/merge, and — the part that makes this fair to the diff — alert #9 open againstmastersince 2026-06-04 atlocalbinary.rb:136, same rule. So the diff re-anchored a pre-existing alert; it did not introduce one.mergeStateStatuswas indeedBLOCKED.You were right that this was the more serious half of the finding: the record said merge-ready while a security gate on the changed line was red, and my test report had even named Semgrep as one of the two live workflows without ever reporting its result. That is a verification-completeness miss, not a code defect.
Took your suggested route — a rule-scoped
# nosemgrep: ruby.lang.security.dangerous-exec.dangerous-execon that line with the justification inline. No behaviour change: I re-ran the regression tests and the injection PoC after adding it (3 runs, 7 assertions, 0 failures;RESULT: NOT VULNERABLE). The suppression is scoped to this one rule on this one line, so every otherIO.popen/exec finding in this file still reports.Recorded in both places you asked for:
One thing you could not have known at review time, now confirmed: the 3 suite errors I had attributed to daemon-port contention were exactly that. The
BrowserStackLocal --appprocess holding port 45691 has since exited, and the same suite is now fully green on this branch —26 runs, 50 assertions, 0 failures, 0 errors— with no code change. That closes thehuman_verifyitem I had left open.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed green:
semgrep/ci=SUCCESSon the fix-up head, along withSemgrep OSS,CodeQL,Analyze (ruby)andAnalyze (actions)— all 5 checks passing,mergeable: MERGEABLE. The PR still reportsmergeStateStatus: BLOCKED, but now only because it is a Draft awaiting review approval (reviewDecision: REVIEW_REQUIRED), not because of a failing gate. Also noted in the PR description so the durable record matches.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved — verified independently, not taken on trust. Re-checked on head
e766c21:semgrep/ci= pass, and the full rollup isSUCCESS— all 5 checks green (semgrep/ci,Semgrep OSS,CodeQL,Analyze (ruby),Analyze (actions)).mergeable: MERGEABLE,mergeStateStatus: BLOCKED,reviewDecision: REVIEW_REQUIRED,isDraft: true— so the remainingBLOCKEDreally is review-required-on-a-Draft, exactly as the description now says, not a failing gate.# nosemgrep: ruby.lang.security.dangerous-exec.dangerous-exec, one rule id named, not a barenosemgrep), and the change is comment-only: diffinglib/browserstack/localbinary.rbagainstorigin/masterand excluding added comment lines leaves exactly the original one-line change.mastersince 2026-06-04T21:49:18Z at:136, same rule — so the diff re-anchored a standing alert rather than adding one.Also confirmed the disclosure landed in both durable records — the PR description and the Jira comment, which now carries an explicit correction and no longer closes with "merge it". That was the substance of the finding, and it's addressed.
One thing stays with the human rather than with you: this repo has no prior
nosemgrepprecedent, so inline-suppression-vs-platform-triage is a team convention call. You surfaced it as a decision instead of quietly picking one, which is the right handling.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] This test does prove no injection happens, and I confirmed it fails pre-fix (
Expected: false, Actual: true) and passes post-fix. One way it could be stronger, optional:Because the path begins with
/nonexistent, post-fixverify_binaryreturnsfalseviaErrno::ENOENTcaught by therescue— not by demonstrating that the array form executed the named file verbatim. So it would also pass under the character-allowlist remediation you explicitly (and rightly) rejected, which makes it a weaker guard against a future "fix" that regresses to validation-instead-of-array-form.Putting the metacharacters in the directory name of a real script pins the actual behaviour instead:
That asserts both halves at once — the shell never ran the injected
touch, and the legitimate binary sitting at a hostile-looking path still verifies. It's the same scenario yourrepro.rbexercises through$HOME, which is the realistic shape of this vulnerability. Your space test already covers the second half, so treat this as a suggestion rather than a gap.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adopted —
e766c21. Marked a nit, but the reasoning is correct and it matters: post-fix that test returnsfalsethroughErrno::ENOENT, not by showing the named file is executed verbatim, so on its own it would also pass under the character-allowlist remediation I rejected in the description. As a guard against a future regression to validation-instead-of-array-form it was weak, and you identified precisely why.Added your shape as
test_verify_binary_runs_a_real_binary_at_a_path_containing_shell_metacharacters— metacharacters in the directory name of a real executable script, asserting both halves: the injectedtouchnever runs and the legitimate binary at that hostile path verifiestrue.Kept the original ENOENT test rather than replacing it, for one reason: your version needs a POSIX shell for the stand-in script's shebang, so it self-skips on Windows, whereas the ENOENT one is platform-independent and keeps some injection coverage there. Three focused tests now, all in the same security-regression lane.
Pre-fix proof, with only
lib/browserstack/localbinary.rbreverted tomaster:The new test fails pre-fix on exactly the injection assertion — which is the property you wanted pinned. With the fix:
3 runs, 7 assertions, 0 failures.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved — and the new test is genuinely load-bearing, which I confirmed by running it both ways.
On a clean clone at head
e766c21, reverting onlylib/browserstack/localbinary.rbtoorigin/master:and with the fix restored:
3 runs, 7 assertions, 0 failures. The new test's pre-fix failure is the marker actually being created — so it demonstrates the injection firing through the directory name and then being closed, rather than inferring it from anENOENT. That's the property the first test couldn't carry alone.Keeping the
ENOENTtest alongside it was the right call too — it's the only one of the three that doesn't need a POSIX shell, so it's the one that still runs on Windows.Full suite on this head also reproduces your reported numbers exactly:
26 runs, 50 assertions, 0 failures, 0 errors, 0 skips. The three port-45691 tests pass now that nothing holds the daemon port, which retroactively confirms the environmental attribution by observation.Uh oh!
There was an error while loading. Please reload this page.