diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 14be170..6f2a906 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -22,33 +22,33 @@ jobs:
steps:
- name: Checkout project
- uses: actions/checkout@v3
+ uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3
- name: Set up JDK 17
- uses: actions/setup-java@v3
+ uses: actions/setup-java@e9fbacdec3bb3b6036605a3e6f7995d66773a8c6 # v3
with:
java-version: "17"
distribution: "corretto"
architecture: "x64"
- name: Setup Android SDK
- uses: android-actions/setup-android@v3
+ uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
with:
- packages: "tools platform-tools build-tools;30.0.3"
+ packages: "platform-tools build-tools;30.0.3"
- name: Get project hash
run: |
git_hash=$(git rev-parse --short "$GITHUB_SHA")
echo "git_hash=$git_hash" >> $GITHUB_ENV
- name: Check format
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3
with:
build-root-directory: play-validations
arguments: spotlessCheck
- name: Build memory-footprint.jar
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3
with:
build-root-directory: play-validations
arguments: memory-footprint:jar -Dmemory-footprint.git_hash=${{ env.git_hash }}
- name: Test memory-footprint
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3
with:
build-root-directory: play-validations
arguments: memory-footprint:test
@@ -57,15 +57,15 @@ jobs:
steps:
- name: Checkout project
- uses: actions/checkout@v3
+ uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3
- name: Set up JDK 17
- uses: actions/setup-java@v3
+ uses: actions/setup-java@e9fbacdec3bb3b6036605a3e6f7995d66773a8c6 # v3
with:
java-version: "17"
distribution: "corretto"
architecture: "x64"
- name: Build the validator
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3
with:
build-root-directory: third_party/wff
arguments: :specification:validator:build
@@ -74,33 +74,33 @@ jobs:
steps:
- name: Checkout project
- uses: actions/checkout@v3
+ uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3
- name: Set up JDK 17
- uses: actions/setup-java@v3
+ uses: actions/setup-java@e9fbacdec3bb3b6036605a3e6f7995d66773a8c6 # v3
with:
java-version: "17"
distribution: "corretto"
architecture: "x64"
- name: Setup Android SDK
- uses: android-actions/setup-android@v3
+ uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
with:
- packages: "tools platform-tools build-tools;30.0.3"
+ packages: "platform-tools build-tools;30.0.3"
- name: Get project hash
run: |
git_hash=$(git rev-parse --short "$GITHUB_SHA")
echo "git_hash=$git_hash" >> $GITHUB_ENV
- name: Check format
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3
with:
build-root-directory: tools
arguments: spotlessCheck
- name: Build wff-optimizer.jar
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3
with:
build-root-directory: tools
arguments: wff-optimizer:jar -Dmemory-footprint.git_hash=${{ env.git_hash }}
- name: Test wff-optimizer
- uses: gradle/actions/setup-gradle@v3
+ uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3
with:
build-root-directory: tools
arguments: wff-optimizer:test
diff --git a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.kt b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.kt
index 62c462b..a3f940b 100644
--- a/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.kt
+++ b/play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/WatchFaceData.kt
@@ -16,17 +16,19 @@
package com.google.wear.watchface.dfx.memory
-import org.w3c.dom.Document
import java.io.ByteArrayInputStream
import java.util.stream.Stream
import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.parsers.SAXParserFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMResult
import javax.xml.transform.sax.SAXSource
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
import kotlin.streams.asSequence
+import org.w3c.dom.Document
import org.xml.sax.InputSource
+import org.xml.sax.SAXException
internal class WatchFaceData private constructor() {
@@ -86,8 +88,32 @@ internal class WatchFaceData private constructor() {
// DWF Runtime. See b/493476140 for details.
val docFactory = DocumentBuilderFactory.newInstance().apply { isNamespaceAware = true }
val doc = docFactory.newDocumentBuilder().newDocument()
+ val saxFactory =
+ try {
+ SAXParserFactory.newDefaultInstance()
+ } catch (_: NoSuchMethodError) {
+ SAXParserFactory.newInstance()
+ }.apply {
+ // Disable DTDs and external entities to prevent XML External Entity (XXE) and
+ // recursive entity expansion (e.g. Billion Laughs) attacks on untrusted inputs.
+ setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
+ setFeature("http://xml.org/sax/features/external-general-entities", false)
+ setFeature("http://xml.org/sax/features/external-parameter-entities", false)
+ }
+ val xmlReader =
+ saxFactory.newSAXParser().xmlReader.apply {
+ try {
+ // Once DTDs and custom entities are disabled above, recursive entity
+ // expansion is impossible, so it is safe to disable these size limits to
+ // allow watch faces with a large number of built-in entities (like ").
+ setProperty("jdk.xml.maxGeneralEntitySizeLimit", 0)
+ setProperty("jdk.xml.totalEntitySizeLimit", 0)
+ } catch (_: SAXException) {
+ // Ignored on parsers that do not support JDK XML limit properties.
+ }
+ }
val inputStream = InputSource(ByteArrayInputStream(xmlData))
- val sax = SAXSource(inputStream)
+ val sax = SAXSource(xmlReader, inputStream)
val transformer = TransformerFactory.newInstance().newTransformer()
transformer.transform(sax, DOMResult(doc))
return doc
diff --git a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/WatchFaceDataTest.java b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/WatchFaceDataTest.java
index 499891f..b4fc160 100644
--- a/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/WatchFaceDataTest.java
+++ b/play-validations/memory-footprint/src/test/java/com/google/wear/watchface/dfx/memory/WatchFaceDataTest.java
@@ -1,17 +1,20 @@
package com.google.wear.watchface.dfx.memory;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.junit.Test;
@@ -86,6 +89,44 @@ public void fromResourcesStream_createsPackageFromWindowsPaths() throws Exceptio
}
}
+ @Test
+ public void fromResourcesStream_parsesWatchFaceWithOver100kEntityReferences() {
+ String repeatedEntities = String.join("", Collections.nCopies(60000, ""a" "));
+ String xml =
+ ""
+ + ""
+ + "";
+ AndroidResource resource =
+ AndroidResource.fromPath(
+ Paths.get("base/res/raw/watchface.xml"),
+ xml.getBytes(StandardCharsets.UTF_8));
+
+ WatchFaceData watchFaceData =
+ WatchFaceData.fromResourcesStream(
+ Stream.of(resource), TEST_EVALUATION_SETTINGS, imageProcessor);
+
+ assertThat(watchFaceData.getWatchFaceDocuments()).hasSize(1);
+ }
+
+ @Test
+ public void fromResourcesStream_rejectsXmlWithDoctype() {
+ String xml =
+ " ]>"
+ + "";
+ AndroidResource resource =
+ AndroidResource.fromPath(
+ Paths.get("base/res/raw/watchface.xml"),
+ xml.getBytes(StandardCharsets.UTF_8));
+
+ assertThrows(
+ Exception.class,
+ () ->
+ WatchFaceData.fromResourcesStream(
+ Stream.of(resource), TEST_EVALUATION_SETTINGS, imageProcessor));
+ }
+
private AndroidResource readPackageFile(String path) throws RuntimeException {
Path rootPath = Paths.get(TEST_PACKAGE_FILES_ROOT);
Path filePath = Paths.get(TEST_PACKAGE_FILES_ROOT, path);