Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in WebAPKs
DescriptionInappropriate implementation in WebAPKs
ComponentWebAPKs
Bug ClassLogic Error
Tracker501535295
Fix commit3733bb9b463d (chromium/src) +86/-14
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
for
components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java
modified
if
components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java
modified

Files Changed

  • components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java
  • components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignatureTest.java
From 3733bb9b463d701e71123a37538aedbb470c3079 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Thu, 30 Apr 2026 09:34:22 -0700
Subject: [PATCH] WebAPK: Fix sign-extension bugs in ZIP parser

This CL fixes sign-extension bugs in WebApkVerifySignature's read2() and
read4() methods. These methods were incorrectly returning signed values,
leading to incorrect offset calculations when the high bit was set.

Key changes:

- Updated read2() to return an unsigned 16-bit int (masked with 0xFFFF).
- Split 4-byte reading into two methods:
  - read4Raw(): Returns a raw int for signatures and bit sequences. This
    is sufficient as standard ZIP signatures do not have the high bit
    set, and scanning (e.g. in findEOCDStart) remains robust.
  - read4InIntRange(): Returns a range-validated int for offsets and
    sizes. It throws IndexOutOfBoundsException if the value is negative
    (bit 31 is set), effectively enforcing the 2GB ByteBuffer limit.
- Kept ZIP offsets and sizes as int (mCentralDirOffset,
  mEndOfCentralDirOffset, Block.mPosition, Block.mCompressedSize) since
  they are now validated upon reading.
- Added regression tests for read2() and read4InIntRange().

Fixed: 501535295
Change-Id: I002f1d3c2fc18e55732fe14108496792f5e5b52b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7801363
Commit-Queue: Andrew Paseltiner <[email protected]>
Reviewed-by: Yaron Friedman <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1623279}
---

diff --git a/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java b/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java
index 52aa2e2..f5ea7eab1 100644
--- a/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java
+++ b/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java
@@ -9,6 +9,7 @@
 import static java.nio.ByteOrder.LITTLE_ENDIAN;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
 
 import org.chromium.base.Log;
 import org.chromium.build.annotations.NullMarked;
@@ -60,16 +61,16 @@
     private static final String TAG = "WebApkVerifySignature";
 
     /** End Of Central Directory Signature. */
-    private static final long EOCD_SIG = 0x06054b50;
+    private static final int EOCD_SIG = 0x06054b50;
 
     /** Central Directory Signature. */
-    private static final long CD_SIG = 0x02014b50;
+    private static final int CD_SIG = 0x02014b50;
 
     /** Local File Header Signature. */
-    private static final long LFH_SIG = 0x04034b50;
+    private static final int LFH_SIG = 0x04034b50;
 
     /** Data descriptor Signature. */
-    private static final long DATA_DESCRIPTOR_SIG = 0x08074b50;
+    private static final int DATA_DESCRIPTOR_SIG = 0x08074b50;
 
     /** Minimum end-of-central-directory size in bytes, including variable length file comment. */
     private static final int MIN_EOCD_SIZE = 22;
@@ -304,7 +305,7 @@
         seek(start + 10);
         mRecordCount = read2(); // Number of Central Directory records
         seekDelta(4); // Size of central directory
-        mCentralDirOffset = read4(); // as bytes from start of file.
+        mCentralDirOffset = read4InIntRange(); // as bytes from start of file.
         int commentLength = read2();
         mComment = readString(commentLength);
         if (mBuffer.position() < mBuffer.limit()) {
@@ -324,7 +325,7 @@
         mBlocks = new ArrayList<>(mRecordCount);
         seek(mCentralDirOffset);
         for (int i = 0; i < mRecordCount; i++) {
-            int signature = read4();
+            int signature = read4Raw();
             if (signature != CD_SIG) {
                 Log.d(TAG, "Missing Central Directory Signature");
                 return Error.BAD_APK;
@@ -332,13 +333,13 @@
             // CreatorVersion(2), ReaderVersion(2), Flags(2), CompressionMethod(2)
             // ModifiedTime(2), ModifiedDate(2), CRC32(4) = 16 bytes
             seekDelta(16);
-            int compressedSize = read4();
+            int compressedSize = read4InIntRange();
             seekDelta(4); // uncompressed size
             int fileNameLength = read2();
             int extraLen = read2();
             int fileCommentLength = read2();
             seekDelta(8); // DiskNumberStart(2), Internal Attrs(2), External Attrs(4)
-            int offset = read4();
+            int offset = read4InIntRange();
             String filename = readString(fileNameLength);
             seekDelta(extraLen + fileCommentLength);
             if (fileCommentLength > MAX_FILE_COMMENT_LENGTH) {
@@ -363,7 +364,7 @@
             }
 
             seek(block.mPosition);
-            int signature = read4();
+            int signature = read4Raw();
             if (signature != LFH_SIG) {
                 Log.d(TAG, "LFH Signature missing");
                 return Error.BAD_APK;
@@ -383,7 +384,7 @@
             lastByte = block.mPosition + block.mHeaderSize + block.mCompressedSize;
             if ((flags & 0x8) != 0) {
                 seek(lastByte);
-                if (read4() == DATA_DESCRIPTOR_SIG) {
+                if (read4Raw() == DATA_DESCRIPTOR_SIG) {
                     // Data descriptor, style 1: sig(4), crc-32(4), compressed size(4),
                     // uncompressed size(4) = 16 bytes
                     lastByte += 16;
@@ -422,7 +423,7 @@
         int minSearchOffset = Math.max(0, offset - MAX_EOCD_SIZE);
         for (; offset >= minSearchOffset; offset--) {
             seek(offset);
-            if (read4() == EOCD_SIG) {
+            if (read4Raw() == EOCD_SIG) {
                 // found!
                 return offset;
             }
@@ -453,8 +454,11 @@
      *
      * @return short value read (as an int).
      */
-    private int read2() {
-        return mBuffer.getShort();
+    @VisibleForTesting
+    int read2() {
+        // Mask with 0xFFFF to treat the short as an unsigned 16-bit integer and avoid sign
+        // extension.
+        return mBuffer.getShort() & 0xFFFF;
     }
 
     /**
@@ -462,10 +466,26 @@
      *
      * @return value read.
      */
-    private int read4() {
+    private int read4Raw() {
         return mBuffer.getInt();
     }
 
+    /**
+     * Reads four bytes as an int.
+     *
+     * @return value read.
+     */
+    @VisibleForTesting
+    int read4InIntRange() {
+        int val = read4Raw();
+        if (val < 0) {
+            // Mask with 0xFFFFFFFFL to treat the int as an unsigned 32-bit integer for the error
+            // message.
+            throw new IndexOutOfBoundsException("32-bit value too large: " + (val & 0xFFFFFFFFL));
+        }
+        return val;
+    }
+
     /** Read {@link length} many bytes into a string. */
     private String readString(int length) {
         if (length <= 0) {
diff --git a/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignatureTest.java b/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignatureTest.java
index dd38136..f9f715e 100644
--- a/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignatureTest.java
+++ b/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignatureTest.java
@@ -17,6 +17,8 @@
 import org.chromium.testing.local.TestDir;
 
 import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.MappedByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.file.Files;
@@ -43,6 +45,56 @@
     }
 
     @Test
+    public void testRead2() {
+        byte[] data = {
+            (byte) 0x01, (byte) 0x00, // 1
+            (byte) 0xFF, (byte) 0x7F, // 32767
+            (byte) 0x00, (byte) 0x80, // 32768
+            (byte) 0xFF, (byte) 0xFF // 65535
+        };
+        ByteBuffer buf = ByteBuffer.wrap(data);
+        buf.order(ByteOrder.LITTLE_ENDIAN);
+        WebApkVerifySignature v = new WebApkVerifySignature(buf);
+
+        assertEquals(1, v.read2());
+        assertEquals(32767, v.read2());
+        assertEquals(32768, v.read2());
+        assertEquals(65535, v.read2());
Loading diff…

Original Bug Report

reported by [email protected]

Potential WebAPK signature bypass via ZIP parser differential in WebApkVerifySignature

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.

Overview: A sign-extension bug in Chrome’s WebApkVerifySignature ZIP parser causes 16-bit fields to be evaluated as negative values if the high bit is set. This creates a parser differential with Android’s libziparchive, potentially allowing an attacker to craft a WebAPK where Chrome hashes a legitimate payload while Android extracts a malicious one.

Affected files:

  • components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java

Estimated timestamp from git blame: 2017-06-08

Root Cause

In components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkVerifySignature.java, Chrome uses a custom ZIP parser to cryptographically hash WebAPK contents and verify their signatures. The parser uses a helper method, read2(), to read 16-bit fields from the ZIP structure:

    private int read2() {
        return mBuffer.getShort();
    }

Java’s short primitive is a signed 16-bit integer. When read2() encounters a raw value of 0x8000 or greater, it is interpreted as a negative number. Because read2() returns an int, this negative short is sign-extended into a negative integer (e.g., 0xFFFF becomes -1). Standard ZIP 16-bit length fields are unsigned.

Impact and Exploitation Scenario

This flaw creates a severe parser differential between Chrome’s signature verification and Android’s OS-level APK extraction (which uses the C++ libziparchive and correctly treats these fields as unsigned). An attacker could exploit this discrepancy to bypass WebAPK signature validation.

Potential Attack Scenario: Note: This is a theoretical attack sequence based on static code analysis; a live proof-of-concept has not been executed.

  1. An attacker crafts a malicious APK acting as a WebAPK.
  2. In the Local File Header (LFH) for a critical file, the attacker sets the 16-bit extraFieldLength field to 0xFFFF.
  3. When Chrome attempts to verify the WebAPK (WebApkValidator.verifyCommentSignedWebApk), it parses the LFH.
  4. Chrome reads extraFieldLength via read2(), which returns -1.
  5. Chrome calculates the data start offset using this negative value: block.mHeaderSize = 30 + fileNameLength + (-1).
  6. A standard unsigned parser (like Android’s libziparchive) evaluates this as 30 + fileNameLength + 65535.
  7. This creates an exact 65536-byte discrepancy between where Chrome believes the file data begins and where Android believes it begins.
  8. Chrome’s calculateHash() seeks to its calculated (earlier) offset and hashes the subsequent data. If the attacker places a “legitimate” WebAPK payload at this offset, Chrome validates the signature successfully.
  9. When Android installs the APK, libziparchive seeks to its calculated (later) offset and extracts the data. If the attacker places a “malicious” payload at this offset, Android installs the malicious code.
  10. The resulting installed app gains WebAPK privileges for the spoofed origin (e.g., launching without a URL bar, native URL handling) despite containing unauthorized code.

Update the read2() and read4() helper methods in WebApkVerifySignature.java to explicitly mask the return values, ensuring they are evaluated as unsigned integers:

    private int read2() {
        return mBuffer.getShort() & 0xFFFF;
    }

    private long read4() {
        return mBuffer.getInt() & 0xFFFFFFFFL;
    }

(Note: If read4() is updated to return long, dependent variables and method signatures handling 32-bit ZIP fields must also be updated to long to prevent truncation.)

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker