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);