Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fb6ff598 authored by Azhara Assanova's avatar Azhara Assanova Committed by Gerrit Code Review
Browse files

Merge "Disallow dangerous zip entry names"

parents 757c168f ef3688b9
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
@@ -16,10 +16,14 @@

package com.android.internal.os;

import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL;

import android.annotation.TestApi;
import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.ApplicationErrorReport;
import android.app.IActivityManager;
import android.app.compat.CompatChanges;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.type.DefaultMimeMapFactory;
import android.net.TrafficStats;
@@ -36,6 +40,7 @@ import com.android.internal.logging.AndroidConfig;

import dalvik.system.RuntimeHooks;
import dalvik.system.VMRuntime;
import dalvik.system.ZipPathValidator;

import libcore.content.type.MimeMap;

@@ -260,9 +265,30 @@ public class RuntimeInit {
         */
        TrafficStats.attachSocketTagger();

        /*
         * Initialize the zip path validator callback depending on the targetSdk.
         */
        initZipPathValidatorCallback();

        initialized = true;
    }

    /**
     * If targetSDK >= U: set the safe zip path validator callback which disallows dangerous zip
     * entry names.
     * Otherwise: clear the callback to the default validation.
     *
     * @hide
     */
    @TestApi
    public static void initZipPathValidatorCallback() {
        if (CompatChanges.isChangeEnabled(VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL)) {
            ZipPathValidator.setCallback(new SafeZipPathValidatorCallback());
        } else {
            ZipPathValidator.clearCallback();
        }
    }

    /**
     * Returns an HTTP user agent of the form
     * "Dalvik/1.1.0 (Linux; U; Android Eclair Build/MAIN)".
+67 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.os;

import android.annotation.NonNull;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.os.Build;

import dalvik.system.ZipPathValidator;

import java.io.File;
import java.util.zip.ZipException;

/**
 * A child implementation of the {@link dalvik.system.ZipPathValidator.Callback} that removes the
 * risk of zip path traversal vulnerabilities.
 *
 * @hide
 */
public class SafeZipPathValidatorCallback implements ZipPathValidator.Callback {
    /**
     * This change targets zip path traversal vulnerabilities by throwing
     * {@link java.util.zip.ZipException} if zip path entries contain ".." or start with "/".
     * <p>
     * The exception will be thrown in {@link java.util.zip.ZipInputStream#getNextEntry} or
     * {@link java.util.zip.ZipFile#ZipFile(String)}.
     * <p>
     * This validation is enabled for apps with targetSDK >= U.
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    public static final long VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL = 242716250L;

    @Override
    public void onZipEntryAccess(@NonNull String path) throws ZipException {
        if (path.startsWith("/")) {
            throw new ZipException("Invalid zip entry path: " + path);
        }
        if (path.contains("..")) {
            // If the string does contain "..", break it down into its actual name elements to
            // ensure it actually contains ".." as a name, not just a name like "foo..bar" or even
            // "foo..", which should be fine.
            File file = new File(path);
            while (file != null) {
                if (file.getName().equals("..")) {
                    throw new ZipException("Invalid zip entry path: " + path);
                }
                file = file.getParentFile();
            }
        }
    }
}
+244 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.os;

import static org.junit.Assert.assertThrows;

import android.compat.testing.PlatformCompatChangeRule;

import androidx.test.runner.AndroidJUnit4;

import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * Test SafeZipPathCallback.
 */
@RunWith(AndroidJUnit4.class)
public class SafeZipPathValidatorCallbackTest {
    @Rule
    public TestRule mCompatChangeRule = new PlatformCompatChangeRule();

    @Before
    public void setUp() {
        RuntimeInit.initZipPathValidatorCallback();
    }

    @Test
    @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
    public void testNewZipFile_whenZipFileHasDangerousEntriesAndChangeEnabled_throws()
            throws Exception {
        final String[] dangerousEntryNames = {
            "../foo.bar",
            "foo/../bar.baz",
            "foo/../../bar.baz",
            "foo.bar/..",
            "foo.bar/../",
            "..",
            "../",
            "/foo",
        };
        for (String entryName : dangerousEntryNames) {
            final File tempFile = File.createTempFile("smdc", "zip");
            try {
                writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName);

                assertThrows(
                        "ZipException expected for entry: " + entryName,
                        ZipException.class,
                        () -> {
                            new ZipFile(tempFile);
                        });
            } finally {
                tempFile.delete();
            }
        }
    }

    @Test
    @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
    public void
            testZipInputStreamGetNextEntry_whenZipFileHasDangerousEntriesAndChangeEnabled_throws()
                    throws Exception {
        final String[] dangerousEntryNames = {
            "../foo.bar",
            "foo/../bar.baz",
            "foo/../../bar.baz",
            "foo.bar/..",
            "foo.bar/../",
            "..",
            "../",
            "/foo",
        };
        for (String entryName : dangerousEntryNames) {
            byte[] badZipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName);
            try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(badZipBytes))) {
                assertThrows(
                        "ZipException expected for entry: " + entryName,
                        ZipException.class,
                        () -> {
                            zis.getNextEntry();
                        });
            }
        }
    }

    @Test
    @EnableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
    public void testNewZipFile_whenZipFileHasNormalEntriesAndChangeEnabled_doesNotThrow()
            throws Exception {
        final String[] normalEntryNames = {
            "foo", "foo.bar", "foo..bar",
        };
        for (String entryName : normalEntryNames) {
            final File tempFile = File.createTempFile("smdc", "zip");
            try {
                writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName);
                try {
                    new ZipFile((tempFile));
                } catch (ZipException e) {
                    throw new AssertionError("ZipException not expected for entry: " + entryName);
                }
            } finally {
                tempFile.delete();
            }
        }
    }

    @Test
    @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
    public void
            testZipInputStreamGetNextEntry_whenZipFileHasNormalEntriesAndChangeEnabled_doesNotThrow()
                    throws Exception {
        final String[] normalEntryNames = {
            "foo", "foo.bar", "foo..bar",
        };
        for (String entryName : normalEntryNames) {
            byte[] zipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName);
            try {
                ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes));
                zis.getNextEntry();
            } catch (ZipException e) {
                throw new AssertionError("ZipException not expected for entry: " + entryName);
            }
        }
    }

    @Test
    @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
    public void
            testNewZipFile_whenZipFileHasNormalAndDangerousEntriesAndChangeDisabled_doesNotThrow()
                    throws Exception {
        final String[] entryNames = {
            "../foo.bar",
            "foo/../bar.baz",
            "foo/../../bar.baz",
            "foo.bar/..",
            "foo.bar/../",
            "..",
            "../",
            "/foo",
            "foo",
            "foo.bar",
            "foo..bar",
        };
        for (String entryName : entryNames) {
            final File tempFile = File.createTempFile("smdc", "zip");
            try {
                writeZipFileOutputStreamWithEmptyEntry(tempFile, entryName);
                try {
                    new ZipFile((tempFile));
                } catch (ZipException e) {
                    throw new AssertionError("ZipException not expected for entry: " + entryName);
                }
            } finally {
                tempFile.delete();
            }
        }
    }

    @Test
    @DisableCompatChanges({SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL})
    public void
            testZipInputStreamGetNextEntry_whenZipFileHasNormalAndDangerousEntriesAndChangeDisabled_doesNotThrow()
                    throws Exception {
        final String[] entryNames = {
            "../foo.bar",
            "foo/../bar.baz",
            "foo/../../bar.baz",
            "foo.bar/..",
            "foo.bar/../",
            "..",
            "../",
            "/foo",
            "foo",
            "foo.bar",
            "foo..bar",
        };
        for (String entryName : entryNames) {
            byte[] zipBytes = getZipBytesFromZipOutputStreamWithEmptyEntry(entryName);
            try {
                ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes));
                zis.getNextEntry();
            } catch (ZipException e) {
                throw new AssertionError("ZipException not expected for entry: " + entryName);
            }
        }
    }

    private void writeZipFileOutputStreamWithEmptyEntry(File tempFile, String entryName)
            throws IOException {
        FileOutputStream tempFileStream = new FileOutputStream(tempFile);
        writeZipOutputStreamWithEmptyEntry(tempFileStream, entryName);
        tempFileStream.close();
    }

    private byte[] getZipBytesFromZipOutputStreamWithEmptyEntry(String entryName)
            throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        writeZipOutputStreamWithEmptyEntry(bos, entryName);
        return bos.toByteArray();
    }

    private void writeZipOutputStreamWithEmptyEntry(OutputStream os, String entryName)
            throws IOException {
        ZipOutputStream zos = new ZipOutputStream(os);
        ZipEntry entry = new ZipEntry(entryName);
        zos.putNextEntry(entry);
        zos.write(new byte[2]);
        zos.closeEntry();
        zos.close();
    }
}