diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a20aa95799..2ae769f0e56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: branches: - master - beta + - private-preview - sdk-release/** - feature/** tags: @@ -125,6 +126,9 @@ jobs: echo "JAVA_TEST_HOME=$JAVA_TEST_HOME" - uses: stripe/openapi/actions/stripe-mock@master + with: + # Used to determine if stripe-mock runs in beta mode + base: ${{ github.base_ref == 'private-preview' && 'beta' || github.ref_name == 'private-preview' && 'beta' || contains(github.ref_name, '-alpha.') && 'beta' || github.base_ref || github.ref_name }} - name: Run test suite run: just test diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4bbbb9a2f..f44737e45f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ This release changes the pinned API version to 2026-08-26.preview. * Add support for event notification `V2SignalsAccountEvaluationCompleteEvent` with related object `v2.signals.AccountEvaluation` * [#2267](https://github.com/stripe/stripe-java/pull/2267) Add non-verified methods to managed handlers +## 33.4.1 - 2026-09-01 +* [#2284](https://github.com/stripe/stripe-java/pull/2284) Harden API requestor code against malicious URLs + ## 33.4.0 - 2026-08-26 This release changes the pinned API version to 2026-08-26.dahlia. diff --git a/CODEGEN_VERSION b/CODEGEN_VERSION index 308945cbf59..635c5cd381b 100644 --- a/CODEGEN_VERSION +++ b/CODEGEN_VERSION @@ -1 +1 @@ -baff58c9d515cdd5f5c3231d101989d588788c6f \ No newline at end of file +e2d3e8255ba431b6d05e4a423941a157e947e763 \ No newline at end of file diff --git a/OPENAPI_VERSION b/OPENAPI_VERSION index 28d67d2bfca..ef8e197a3ce 100644 --- a/OPENAPI_VERSION +++ b/OPENAPI_VERSION @@ -1 +1 @@ -v2442 \ No newline at end of file +v2459 \ No newline at end of file diff --git a/src/main/java/com/stripe/model/v2/core/EventNotification.java b/src/main/java/com/stripe/model/v2/core/EventNotification.java index 96f637d4544..064e00ac006 100644 --- a/src/main/java/com/stripe/model/v2/core/EventNotification.java +++ b/src/main/java/com/stripe/model/v2/core/EventNotification.java @@ -144,9 +144,14 @@ private RawRequestOptions getRequestOptions() { /* retrieves the full payload for an event. Protected because individual push classes use it, but type it correctly */ protected Event fetchEvent() throws StripeException { + // `id` comes from the notification body, so encode it the way the generated + // services do -- otherwise it can inject extra path or query segments. StripeResponse response = client.rawRequest( - RequestMethod.GET, String.format("/v2/core/events/%s", id), null, getRequestOptions()); + RequestMethod.GET, + String.format("/v2/core/events/%s", ApiResource.urlEncodeId(id)), + null, + getRequestOptions()); return (Event) client.deserialize(response.body(), ApiMode.V2); } diff --git a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java index b04b29f1fcb..9b8e3fdf19b 100644 --- a/src/main/java/com/stripe/net/LiveStripeResponseGetter.java +++ b/src/main/java/com/stripe/net/LiveStripeResponseGetter.java @@ -475,6 +475,27 @@ public void validateRequestOptions(RequestOptions options) { } } + /** + * Asserts that a request path is origin-relative: that it begins with a single {@code "/"}. + * + *
The absolute URL is built by concatenating a base URL onto this path, and no base URL ends + * in a slash. A path like {@code "@evil.example/v1/x"} or {@code ".evil.example/v1/x"} would + * modify the resulting host and direct the request (including the API key) to a non-Stripe host. + * + *
Because some relative urls arrive from potentially untrusted sources (like webhook bodies), + * we have to be a little defensive. + * + *
So, we require that a path starts with a leading slash. Deliberately not using {@link
+ * java.net.URI} to parse -- it enforces RFC 2396 strictly and would reject paths containing
+ * characters that callers have always been able to send.
+ */
+ static void validatePath(String path) {
+ if (path == null || !path.startsWith("/") || path.startsWith("//")) {
+ throw new IllegalArgumentException(
+ "Request path must begin with a single \"/\", got: " + path);
+ }
+ }
+
private String fullUrl(BaseApiRequest apiRequest) {
BaseAddress baseAddress = apiRequest.getBaseAddress();
RequestOptions options = apiRequest.getOptions();
@@ -499,6 +520,7 @@ private String fullUrl(BaseApiRequest apiRequest) {
if (options != null && options.getBaseUrl() != null) {
baseUrl = options.getBaseUrl();
}
+ validatePath(relativeUrl);
return String.format("%s%s", baseUrl, relativeUrl);
}
}
diff --git a/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java
new file mode 100644
index 00000000000..82417d7448e
--- /dev/null
+++ b/src/test/java/com/stripe/net/DiscriminatedUnionSerializationTest.java
@@ -0,0 +1,351 @@
+package com.stripe.net;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.stripe.model.StripeObject;
+import java.io.IOException;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+public class DiscriminatedUnionSerializationTest {
+ private final ApiRequestParamsConverter converter = new ApiRequestParamsConverter();
+
+ private final Gson testGson =
+ new GsonBuilder()
+ .registerTypeAdapterFactory(new TestColorTypeAdapterFactory())
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+
+ // ---------------------------------------------------------------------------
+ // Request-side fixtures — standalone union
+ // The parent params hold an Object-typed field that can hold any variant.
+ // ---------------------------------------------------------------------------
+
+ @SuppressWarnings("UnusedVariable")
+ private static class TestCreateParams extends ApiRequestParams {
+ @SerializedName("color")
+ Object color;
+
+ @SerializedName("name")
+ String name;
+ }
+
+ @SuppressWarnings("UnusedVariable")
+ private static class TestRgbColorParams extends ApiRequestParams {
+ @SerializedName("model")
+ String model = "rgb";
+
+ @SerializedName("r")
+ Long r;
+
+ @SerializedName("g")
+ Long g;
+
+ @SerializedName("b")
+ Long b;
+ }
+
+ @SuppressWarnings("UnusedVariable")
+ private static class TestHsvColorParams extends ApiRequestParams {
+ @SerializedName("model")
+ String model = "hsv";
+
+ @SerializedName("h")
+ Long h;
+
+ @SerializedName("s")
+ Long s;
+
+ @SerializedName("v")
+ Long v;
+ }
+
+ // ---------------------------------------------------------------------------
+ // Request-side fixtures — inline union
+ // The parent params hold the discriminator and each variant's fields directly.
+ // ---------------------------------------------------------------------------
+
+ @SuppressWarnings("UnusedVariable")
+ private static class TestInlineParams extends ApiRequestParams {
+ @SerializedName("type")
+ String type;
+
+ @SerializedName("card")
+ TestCardParams card;
+
+ @SerializedName("bank")
+ TestBankParams bank;
+ }
+
+ @SuppressWarnings("UnusedVariable")
+ private static class TestCardParams extends ApiRequestParams {
+ @SerializedName("number")
+ String number;
+
+ @SerializedName("exp_month")
+ Long expMonth;
+ }
+
+ @SuppressWarnings("UnusedVariable")
+ private static class TestBankParams extends ApiRequestParams {
+ @SerializedName("routing_number")
+ String routingNumber;
+
+ @SerializedName("account_number")
+ String accountNumber;
+ }
+
+ // ---------------------------------------------------------------------------
+ // Response-side fixtures
+ // ---------------------------------------------------------------------------
+
+ private static class TestColorEntity extends StripeObject {
+ @SerializedName("model")
+ String model;
+ }
+
+ private static class TestRgbColorEntity extends TestColorEntity {
+ @SerializedName("r")
+ Long r;
+
+ @SerializedName("g")
+ Long g;
+
+ @SerializedName("b")
+ Long b;
+ }
+
+ private static class TestHsvColorEntity extends TestColorEntity {
+ @SerializedName("h")
+ Long h;
+
+ @SerializedName("s")
+ Long s;
+
+ @SerializedName("v")
+ Long v;
+ }
+
+ private static class TestColorTypeAdapterFactory implements TypeAdapterFactory {
+ @Override
+ @SuppressWarnings("unchecked")
+ public