Repository: googleapis/google-http-java-client
Pull request #1819 (2023-02-21 15:52:51)

feat: GsonFactory to have read leniency option
Fixes b/269515918, where the prefix `)]}'\n` for Cross-site script inclusion (XSSI) didn't work for GsonFactory.

Users will call `GsonFactory.getDefaultInstance().withReadLeniency()` when they want this library to process JSON input that may have the prefix.

- For this pull request, it controlls only the leniency when reading input JSON. This design `withReadLeniency()` gives flexibility for us to choose writeLeniency in future if necessary. (It's possible that read leniency is true and write leniency is false.)
- Because this creates a copy of the instance, it does not affect existing usage of GsonFactory. (No global state in GsonFactory)


suztomo: lint (non required) fails:

```
Error:  Found 1 non-complying files, failing build
Error:  To fix formatting errors, run "mvn com.coveo:fmt-maven-plugin:format"
Error:  Non complying file: /home/runner/work/google-http-java-client/google-http-java-client/google-http-client/src/test/java/com/google/api/client/http/HttpRequestTracingTest.java
```

This is irrelevant to this pull request.

Patch:
diff --git a/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonFactory.java b/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonFactory.java
index f02ba0f3..6c89dd16 100644
--- a/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonFactory.java
+++ b/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonFactory.java
@@ -52,12 +52,23 @@ public class GsonFactory extends JsonFactory {
     return InstanceHolder.INSTANCE;
   }
 
+  /** Controls the behavior of leniency in reading JSON value */
+  private boolean readLeniency = false;
+
   /** Holder for the result of {@link #getDefaultInstance()}. */
   @Beta
   static class InstanceHolder {
     static final GsonFactory INSTANCE = new GsonFactory();
   }
 
+  // Keeping the default, non-arg constructor for backward compatibility. Users should use
+  // getDefaultInstance() or builder() instead.
+  public GsonFactory() {}
+
+  private GsonFactory(Builder builder) {
+    readLeniency = builder.readLeniency;
+  }
+
   @Override
   public JsonParser createJsonParser(InputStream in) {
     return createJsonParser(new InputStreamReader(in, StandardCharsets.UTF_8));
@@ -90,4 +101,36 @@ public class GsonFactory extends JsonFactory {
   public JsonGenerator createJsonGenerator(Writer writer) {
     return new GsonGenerator(this, new JsonWriter(writer));
   }
+
+  /** Returns true if it is lenient to input JSON value. */
+  boolean getReadLeniency() {
+    return readLeniency;
+  }
+
+  /** Returns the builder * */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder for GsonFactory. */
+  public static final class Builder {
+    // Do not directly call this constructor
+    private Builder() {}
+
+    private boolean readLeniency = false;
+
+    /**
+     * Set to {@code true} when you want to the JSON parser to be lenient to reading JSON value. By
+     * default, it is {@code false}.
+     */
+    public Builder setReadLeniency(boolean readLeniency) {
+      this.readLeniency = readLeniency;
+      return this;
+    }
+
+    /** Builds GsonFactory instance. */
+    public GsonFactory build() {
+      return new GsonFactory(this);
+    }
+  }
 }
diff --git a/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonParser.java b/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonParser.java
index dc90369c..cee6fadc 100644
--- a/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonParser.java
+++ b/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonParser.java
@@ -43,7 +43,7 @@ class GsonParser extends JsonParser {
   GsonParser(GsonFactory factory, JsonReader reader) {
     this.factory = factory;
     this.reader = reader;
-    reader.setLenient(false);
+    reader.setLenient(factory.getReadLeniency());
   }
 
   @Override


Test patch:
diff --git a/google-http-client-gson/src/test/java/com/google/api/client/json/gson/GsonFactoryTest.java b/google-http-client-gson/src/test/java/com/google/api/client/json/gson/GsonFactoryTest.java
index 05ab5bf0..5d166e68 100644
--- a/google-http-client-gson/src/test/java/com/google/api/client/json/gson/GsonFactoryTest.java
+++ b/google-http-client-gson/src/test/java/com/google/api/client/json/gson/GsonFactoryTest.java
@@ -14,10 +14,16 @@
 
 package com.google.api.client.json.gson;
 
+import com.google.api.client.json.GenericJson;
 import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.JsonObjectParser;
 import com.google.api.client.json.JsonParser;
 import com.google.api.client.test.json.AbstractJsonFactoryTest;
+import com.google.gson.stream.MalformedJsonException;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 
 /**
@@ -94,4 +100,30 @@ public class GsonFactoryTest extends AbstractJsonFactoryTest {
       assertNotNull(ex.getMessage());
     }
   }
+
+  public final void testReaderLeniency_lenient() throws IOException {
+    JsonObjectParser parser =
+        new JsonObjectParser(GsonFactory.builder().setReadLeniency(true).build());
+
+    // This prefix in JSON body is used to prevent Cross-site script inclusion (XSSI).
+    InputStream inputStream =
+        new ByteArrayInputStream((")]}'\n" + JSON_ENTRY_PRETTY).getBytes(StandardCharsets.UTF_8));
+    GenericJson json = parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class);
+
+    assertEquals("foo", json.get("title"));
+  }
+
+  public final void testReaderLeniency_not_lenient_by_default() throws IOException {
+    JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance());
+
+    try {
+      // This prefix in JSON body is used to prevent Cross-site script inclusion (XSSI).
+      InputStream inputStream =
+          new ByteArrayInputStream((")]}'\n" + JSON_ENTRY_PRETTY).getBytes(StandardCharsets.UTF_8));
+      parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class);
+      fail("The read leniency should fail the JSON input with XSSI prefix.");
+    } catch (MalformedJsonException ex) {
+      assertNotNull(ex.getMessage());
+    }
+  }
 }