-
Notifications
You must be signed in to change notification settings - Fork 9
security: address APS-19018, APS-19019, APS-19020, APS-19024 #82
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?
Changes from all commits
9044b3a
ba24f7b
f335401
b89196e
159c132
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Changelog | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to commit this? |
||
|
|
||
| All notable changes to this project will be documented in this file. | ||
|
|
||
| ## [Unreleased] | ||
|
|
||
| ### Security | ||
|
|
||
| - **SSRF guard on `AutomateClient.getSessionLogs`** — the URL returned by the | ||
| BrowserStack API is now validated before being fetched. Only `https` URLs whose | ||
| host (parsed via `java.net.URI`) ends in `.browserstack.com` are accepted; all | ||
| other URLs throw `AutomateException`. This prevents a maliciously-crafted | ||
| session record from causing the client to issue requests against an attacker | ||
| endpoint. (APS-19018) | ||
|
|
||
| - **`setProxy` is now per-instance** — previously, calling | ||
| `BrowserStackClient.setProxy(...)` mutated a JVM-wide `static HTTP_TRANSPORT` | ||
| field, so a proxy configured on one client instance leaked to every other | ||
| instance running in the same JVM (and a non-proxied client could end up using | ||
| another tenant's proxy). The transport is now an instance field. **Anyone | ||
| whose code relied on cross-instance proxy state must now call `setProxy` on | ||
| each `BrowserStackClient` / `AutomateClient` they construct.** (APS-19019) | ||
|
|
||
| - **`checkAuthState` now throws when EITHER credential is missing** — previously | ||
| the guard only fired when both `username` and `accessKey` were `null`, | ||
| meaning a client with one credential set and one missing would proceed and | ||
| produce a malformed `Authorization: Basic` header. The condition is now | ||
| `username == null || accessKey == null`. (APS-19019) | ||
|
|
||
|
|
||
| - **SSRF guard on JVM-property base URL override** — both `AutomateClient` and | ||
| `AppAutomateClient` constructors now validate the result of | ||
| `System.getProperty("browserstack.automate.api", ...)` / | ||
| `System.getProperty("browserstack.app-automate.api", ...)` before passing it | ||
| to the parent constructor. Only `https` URLs whose host (parsed via | ||
| `java.net.URI`) ends in `.browserstack.com` are accepted; all other values | ||
| throw `IllegalArgumentException`. This prevents an attacker who can set JVM | ||
| properties from redirecting every signed API request — and the Basic Auth | ||
| credentials those requests carry — to an attacker-controlled host. (APS-19024) | ||
|
|
||
| - **Path canonicalization in `AppAutomateClient.uploadApp`** — the file path is | ||
| now resolved via `File.getCanonicalFile()` before applying the `.apk` / | ||
| `.ipa` extension check, and the canonical name (not the original input) is | ||
| what the extension check sees. This prevents a symlink such as | ||
| `/tmp/legit.apk -> /etc/passwd` from passing the suffix-only check and | ||
| causing an arbitrary local file to be streamed to the upload endpoint. | ||
| (APS-19024) | ||
|
|
||
| ### CI / Release | ||
|
|
||
| - **GitHub Actions hardening** — `.github/workflows/ci.yml` actions are now | ||
| pinned to commit SHAs (`actions/checkout@v4.3.1`, | ||
| `actions/setup-java@v4.8.0`) and a top-level `permissions: contents: read` | ||
| block has been added. (APS-19020) | ||
|
|
||
| - **Maven release auto-promote disabled** — `nexus-staging-maven-plugin`'s | ||
| `autoReleaseAfterClose` is now `false`. Staged releases must be manually | ||
| promoted via OSSRH after a `mvn release:perform`. See the BrowserStack | ||
| internal release runbook for the manual promotion step. (APS-19020) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,7 +89,7 @@ | |
| <configuration> | ||
| <serverId>ossrh</serverId> | ||
| <nexusUrl>https://oss.sonatype.org/</nexusUrl> | ||
| <autoReleaseAfterClose>true</autoReleaseAfterClose> | ||
| <autoReleaseAfterClose>false</autoReleaseAfterClose> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this needed? |
||
| </configuration> | ||
| </plugin> | ||
| <plugin> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |
|
|
||
| import java.io.File; | ||
| import java.io.FileNotFoundException; | ||
| import java.io.IOException; | ||
| import java.net.URI; | ||
| import java.util.List; | ||
| import com.browserstack.automate.Automate.BuildStatus; | ||
| import com.browserstack.automate.exception.AppAutomateException; | ||
|
|
@@ -32,7 +34,8 @@ public class AppAutomateClient extends BrowserStackClient implements AppAutomate | |
| * @param accessKey Browserstack accessKey | ||
| */ | ||
| public AppAutomateClient(String username, String accessKey) { | ||
| super(System.getProperty("browserstack.app-automate.api", BASE_URL), username, accessKey); | ||
| super(validateApiBaseUrl(System.getProperty("browserstack.app-automate.api", BASE_URL)), | ||
| username, accessKey); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -71,9 +74,22 @@ public AppUploadResponse uploadApp(String filePath) | |
| throw new FileNotFoundException("File not found at " + filePath); | ||
| } | ||
|
|
||
| if (!filePath.endsWith(".apk") && !filePath.endsWith(".ipa")) { | ||
| // APS-19024 / INJ-003: canonicalize before validating extension. Without this, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add detailed comments in this repo considering this is a customer facing client |
||
| // a symlink such as /tmp/legit.apk -> /etc/passwd would pass a suffix-only | ||
| // check on the supplied path and result in an arbitrary local file being | ||
| // streamed to the upload endpoint. getCanonicalFile() resolves symlinks | ||
| // and ".." segments; we then check the extension on the canonical name. | ||
| final File canonical; | ||
| try { | ||
| canonical = file.getCanonicalFile(); | ||
| } catch (IOException e) { | ||
| throw new AppAutomateException("Unable to canonicalize file path: " + e.getMessage(), 0); | ||
| } | ||
| final String canonicalName = canonical.getName().toLowerCase(); | ||
| if (!canonicalName.endsWith(".apk") && !canonicalName.endsWith(".ipa")) { | ||
| throw new InvalidFileExtensionException("File extension should be only .apk or .ipa."); | ||
| } | ||
| file = canonical; | ||
|
|
||
| MultipartContent content = new MultipartContent().setMediaType( | ||
| new HttpMediaType("multipart/form-data").setParameter("boundary", "__END_OF_PART__")); | ||
|
|
@@ -282,5 +298,46 @@ public List<Session> getSessions(final String buildId, final BuildStatus status) | |
| return getSessions(buildId, status, 0); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * Validates the App Automate API base URL read from the JVM system property | ||
| * {@code browserstack.app-automate.api} (or the built-in default). | ||
| * <p> | ||
| * Defends against SSRF (APS-19024 / INJ-002): an attacker who can set the | ||
| * JVM property could otherwise redirect every signed API request — and the | ||
| * Basic Auth credentials those requests carry — to an attacker-controlled | ||
| * host. Only {@code https} URLs whose host (parsed via | ||
| * {@link java.net.URI#getHost()}) ends in {@code .browserstack.com} are | ||
| * accepted. | ||
| * | ||
| * @param url URL to validate. Must be a syntactically-valid absolute URL. | ||
| * @return the same URL, unchanged, when validation passes. | ||
| * @throws IllegalArgumentException if the URL is null, malformed, uses a | ||
| * non-https scheme, or has a host outside | ||
| * {@code *.browserstack.com}. | ||
| */ | ||
| private static String validateApiBaseUrl(String url) { | ||
| if (url == null) { | ||
| throw new IllegalArgumentException("App Automate API base URL is null"); | ||
| } | ||
|
|
||
| final URI uri; | ||
| try { | ||
| uri = URI.create(url); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new IllegalArgumentException("Malformed App Automate API base URL", e); | ||
| } | ||
|
|
||
| final String scheme = uri.getScheme(); | ||
| if (!"https".equalsIgnoreCase(scheme)) { | ||
| throw new IllegalArgumentException("Insecure App Automate API base URL scheme: " + scheme); | ||
| } | ||
|
|
||
| final String host = uri.getHost(); | ||
| if (host == null || !host.toLowerCase().endsWith(".browserstack.com")) { | ||
| throw new IllegalArgumentException("Untrusted App Automate API base URL host: " + host); | ||
| } | ||
|
|
||
| return url; | ||
| } | ||
| } | ||
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.
Why is this needed?