Harden URL handling, credentials, and CI workflow#82
Harden URL handling, credentials, and CI workflow#82Jimesh-browserstack wants to merge 1 commit intomasterfrom
Conversation
Test & regression evidence (per change)For each change, listing what the fix was tested against and what no-regression evidence we have. Anywhere coverage is incomplete is called out explicitly so reviewers can decide whether to gate on it. APS-19018 — SSRF guard on
|
| Branch | Tests run | Pass | Errors | Notes |
|---|---|---|---|---|
master |
25 | 24 | 1 | AppAutomateClientTest.testGetSession IOOBE — test BS account has no App Automate builds. Pre-existing. |
| this branch | 33 | 32 | 1 | Same single pre-existing error; +8 new tests, all green. |
No new failures; the 1 error is reproducible on master with identical signature. Verified by running mvn test on a clean checkout of master immediately after, comparing line-for-line.
Live smoke harness output (captured 2026-05-07)
[OK] getSession returned. logUrl=https://automate.browserstack.com/builds/.../sessions/.../logs
[OK] getSessionLogs accepted real logUrl. Body length=214392
preview=2026-5-7 13:37:14:126 REQUEST [...] POST /session/.../execute {"t...
[OK] SSRF guard threw on attacker host: AutomateException: Untrusted logs URL host: attacker.example
[OK] Cross-instance isolation: A changed, B untouched.
[OK] setProxy still routes traffic — request failed at proxy: AutomateException: Connect to 127.0.0.1:61783 [/127.0.0.1] failed: Connection refused
ALL SMOKE TESTS PASSED
Honest gaps (intentional, called out for reviewer discretion)
- PR's CI workflow run — still pending at time of this comment. This is the conclusive proof for the APS-19020 SHA-pinning + permissions-block fix. If you'd like to gate merge on CI green (which I'd recommend), please wait for it. If CI doesn't fire automatically, it may need a maintainer's "Approve and run" click.
autoReleaseAfterClose=falseend-to-end — only verifiable by a release dry-run, which is post-merge. The release runbook (Confluence page 538705969) will be updated separately to cover the new manualClose → Releasestep on https://oss.sonatype.org/.- No live BrowserStack session test gate — by design; this is a pure HTTP/JSON SDK, not a session-runner repo. The smoke harness above stands in for what the bs-session test gate would otherwise enforce.
APS-19020 verification — offline (without waiting on CI)Closing the "CI is the only proof" gap from the previous comment with four independent offline checks. Combined, these establish the SHA-pinning + permissions-block change is correct and the build still works, without depending on the PR's own workflow run. 1. Pinned SHAs are the canonical tag commitsResolved both pinned SHAs via the GitHub git-refs API: Both match the SHAs in 2. The exact workflow build command runs green locallyRan Exit 0. Build succeeded with the new 3. Workflow permissions audit —
|
| Step | What it does | Token scope needed |
|---|---|---|
actions/checkout@v4.3.1 |
Clones the repo | contents: read |
actions/setup-java@v4.8.0 (with cache: 'maven') |
Installs JDK; cache uses Actions Cache API (not GITHUB_TOKEN-scoped) | contents: read |
mvn clean install -DskipTests -Dgpg.skip |
Local build, writes only to runner FS | none |
No git push, no gh ..., no artifact upload, no comment posting, no API call that requires a write token. permissions: contents: read is exactly the minimum needed.
4. Cross-reference: existing Semgrep.yml already uses this pattern
The repo's existing .github/workflows/Semgrep.yml runs on every push/PR/cron and uses:
permissions:
contents: readat the top level, with job-level escalation only where it needs security-events: write for SARIF upload. Same pattern, in production, in this repo, today. Our ci.yml follows the same model — minus the SARIF escalation since the build doesn't upload anything.
What's left in the "actually run on GitHub-hosted Ubuntu" gap
The only thing the offline checks don't directly cover is "the pinned actions resolve and execute on a GitHub-hosted Ubuntu runner." Confidence is very high (these are the canonical official SHAs for the named versions, and the sister workflow uses the same Actions + cache mechanism in this repo today), but not 100% until the workflow run completes. The b89196e ci: trigger empty commit was pushed for that purpose; if Actions still hasn't fired by review time, an "Approve and run workflows" maintainer click on the PR will kick it.
<autoReleaseAfterClose>false</autoReleaseAfterClose> continues to be only verifiable by a release dry-run post-merge — that one's a process gate, not a code gate, and is captured in the post-merge runbook plan.
APS-19024 added to this PR — JVM-property base-URL SSRF + uploadApp path traversalBundled into PR #82 per direction (one release cycle covers all four tickets). Commit: Vulnerabilities (from APS-19024)
FixesINJ-002: New public AutomateClient(String username, String accessKey) {
super(validateApiBaseUrl(System.getProperty("browserstack.automate.api", BASE_URL)),
username, accessKey);
}Helper parses via INJ-003: Files touched
Test evidence
The new
Live smoke (real BrowserStack session)The local Breaking-change note (added to PR body)JVM properties |
APS-19024 INJ-002 — extra non-unit verification: no-network-leak smokeThe earlier smoke (case 5) only asserted the guard throws. To close the loop on "no traffic leaks even if the guard regressed in some unforeseen way," I extended
Run output (real BrowserStack creds, real session — full harness): What this strengthens beyond unit tests: unit tests assert the validator's throw + message. This asserts the network-IO contract — the rejection happens before any TCP connection is attempted. The local server is the witness; if a future regression introduced a code path that constructed the URL but only failed on response parsing (or some hypothetical retry that bypassed the guard), the counter would be non-zero and the test would fail. Note on scheme vs host: the local server uses Recipe is committed to the investigation doc at |
| <serverId>ossrh</serverId> | ||
| <nexusUrl>https://oss.sonatype.org/</nexusUrl> | ||
| <autoReleaseAfterClose>true</autoReleaseAfterClose> | ||
| <autoReleaseAfterClose>false</autoReleaseAfterClose> |
There was a problem hiding this comment.
Why is this needed?
|
|
||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 |
There was a problem hiding this comment.
Why is this needed?
| @@ -0,0 +1,59 @@ | |||
| # Changelog | |||
| } | ||
|
|
||
| if (!filePath.endsWith(".apk") && !filePath.endsWith(".ipa")) { | ||
| // APS-19024 / INJ-003: canonicalize before validating extension. Without this, |
There was a problem hiding this comment.
Should we add detailed comments in this repo considering this is a customer facing client
- Validate API base URL read from JVM system properties (browserstack.automate.api / browserstack.app-automate.api): require https scheme and a host under .browserstack.com. - Validate the BrowserStack-issued logs URL in AutomateClient.getSessionLogs before fetching, with the same scheme + host allowlist. - Make HttpTransport per-instance (was a JVM-wide static field) so setProxy on one client does not affect other client instances in the same process. - Tighten BrowserStackClient.checkAuthState so it throws when EITHER the username OR access key is missing (was: only when both were null). - AppAutomateClient.uploadApp now canonicalizes the file path via File.getCanonicalFile() before applying the .apk/.ipa extension check, and uses the canonical File for the upload. - Pin GitHub Actions in .github/workflows/ci.yml to commit SHAs and add a top-level permissions: contents: read block. - Set nexus-staging-maven-plugin autoReleaseAfterClose to false so staged releases must be promoted manually after mvn release:perform. Adds hermetic unit tests for the URL allowlist, credential check, per-instance HttpTransport, and canonical-path extension check.
159c132 to
3995fe7
Compare
|
Closing this PR was unintentional — it happened automatically when the branch was renamed and GitHub deleted the old ref before retargeting. Replacement PR: #83 — same diff, with the feedback on this thread addressed:
Please continue the review on #83. |
Summary
This PR hardens URL handling, credential validation, and CI/release configuration in the
automate-client-javaclient.AutomateClient.getSessionLogshttpsURLs whose host ends with.browserstack.com(parsed viajava.net.URI, not a raw-string suffix) are accepted before fetching the logs URL returned by the API.AutomateClientandAppAutomateClientconstructorsbrowserstack.automate.api/browserstack.app-automate.apiis now validated against the same allowlist before being passed to the parent constructor.BrowserStackClient.setProxystaticfield). Configuring a proxy on one client no longer affects other client instances.BrowserStackClient.checkAuthStateAppAutomateClient.uploadAppFile.getCanonicalFile()before the.apk/.ipaextension check; the canonical file is used for the upload..github/workflows/ci.ymlactions/checkout@v4.3.1,actions/setup-java@v4.8.0); top-levelpermissions: contents: readadded.pom.xmlnexus-staging-maven-pluginautoReleaseAfterCloseis nowfalse— staged releases must be manually promoted aftermvn release:perform.Breaking changes for consumers relying on undocumented behaviour
setProxyis now per-instance. Previously, callingsetProxy(...)on anyBrowserStackClient/AutomateClientmutated a JVM-widestatic HTTP_TRANSPORTfield, so the proxy applied to every other client instance in the same process. After this change,setProxyonly affects the instance it's called on. Anyone whose code relied on cross-instance proxy state must now callsetProxyon each client they construct.checkAuthStatenow throws when EITHER credential is missing. Previously the guard only fired whenusernameANDaccessKeywere bothnull. A client with one credential set and one missing now throwsIllegalStateExceptioninstead of silently producing a malformedAuthorizationheader.browserstack.automate.api/browserstack.app-automate.apiare now allowlist-validated. Anything outsidehttps://*.browserstack.commakes the constructor throwIllegalArgumentException. Effectively a no-op for production consumers; teams pointing at private mocks orlocalhostfor tests will need to update those tests.New manual release step
Because
<autoReleaseAfterClose>is nowfalse, aftermvn release:performthe staged release onhttps://oss.sonatype.org/must be manually promoted (close → release in the OSSRH UI, ormvn nexus-staging:release -DstagingRepositoryId=combrowserstack-NNNN). The internal release runbook will be updated separately.Tests
AutomateClientSecurityTest— 7 tests for the logs URL allowlist and the credential-presence check.ApiBaseUrlAndUploadSecurityTest— 10 tests for the JVM-property base URL allowlist and the canonical-path extension check.BrowserStackClientProxyIsolationTest— 1 test for per-instance HTTP transport isolation.master.