Merge "Sanitize VPN label to prevent HTML injection"
This commit is contained in:
@@ -23,10 +23,15 @@ package {
|
||||
default_applicable_licenses: ["frameworks_base_license"],
|
||||
}
|
||||
|
||||
android_library {
|
||||
name: "VpnDialogsLib",
|
||||
srcs: ["src/**/*.java"],
|
||||
}
|
||||
|
||||
android_app {
|
||||
name: "VpnDialogs",
|
||||
certificate: "platform",
|
||||
privileged: true,
|
||||
srcs: ["src/**/*.java"],
|
||||
static_libs: ["VpnDialogsLib"],
|
||||
platform_apis: true,
|
||||
}
|
||||
|
||||
@@ -100,4 +100,33 @@
|
||||
without any consequences. [CHAR LIMIT=20] -->
|
||||
<string name="dismiss">Dismiss</string>
|
||||
|
||||
<!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs
|
||||
into displaying what they want. The system will attempt to sanitize the label, and if the
|
||||
label is deemed dangerous, then this string is used instead. The first argument is the
|
||||
first 30 characters of the label, and the second argument is the package name of the app.
|
||||
Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read
|
||||
"My VPN app wants to set up a VPN connection...". If the label is very long, then, this
|
||||
will be used to show "VerylongVPNlabel… (com.my.vpn.app) wants to set up a VPN
|
||||
connection...". For this case, the code will refer to sanitized_vpn_label_with_ellipsis.
|
||||
-->
|
||||
<string name="sanitized_vpn_label_with_ellipsis">
|
||||
<xliff:g id="sanitized_vpn_label_with_ellipsis" example="My VPN app">%1$s</xliff:g>… (
|
||||
<xliff:g id="sanitized_vpn_label_with_ellipsis" example="com.my.vpn.app">%2$s</xliff:g>)
|
||||
</string>
|
||||
|
||||
<!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs
|
||||
into displaying what they want. The system will attempt to sanitize the label, and if the
|
||||
label is deemed dangerous, then this string is used instead. The first argument is the
|
||||
label, and the second argument is the package name of the app.
|
||||
Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read
|
||||
"My VPN app wants to set up a VPN connection...". If the VPN label contains HTML tag but
|
||||
the length is not very long, the dialog will show "VpnLabelWith<br>HtmlTag
|
||||
(com.my.vpn.app) wants to set up a VPN connection...". For this case, the code will refer
|
||||
to sanitized_vpn_label.
|
||||
-->
|
||||
<string name="sanitized_vpn_label">
|
||||
<xliff:g id="sanitized_vpn_label" example="My VPN app">%1$s</xliff:g> (
|
||||
<xliff:g id="sanitized_vpn_label" example="com.my.vpn.app">%2$s</xliff:g>)
|
||||
</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -33,6 +33,7 @@ import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.app.AlertActivity;
|
||||
import com.android.internal.net.VpnConfig;
|
||||
|
||||
@@ -40,12 +41,19 @@ public class ConfirmDialog extends AlertActivity
|
||||
implements DialogInterface.OnClickListener, ImageGetter {
|
||||
private static final String TAG = "VpnConfirm";
|
||||
|
||||
// Usually the label represents the app name, 150 code points might be enough to display the app
|
||||
// name, and 150 code points won't cover the warning message from VpnDialog.
|
||||
@VisibleForTesting
|
||||
static final int MAX_VPN_LABEL_LENGTH = 150;
|
||||
|
||||
@VpnManager.VpnType private final int mVpnType;
|
||||
|
||||
private String mPackage;
|
||||
|
||||
private VpnManager mVm;
|
||||
|
||||
private View mView;
|
||||
|
||||
public ConfirmDialog() {
|
||||
this(VpnManager.TYPE_VPN_SERVICE);
|
||||
}
|
||||
@@ -54,6 +62,43 @@ public class ConfirmDialog extends AlertActivity
|
||||
mVpnType = vpnType;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will use the string resource to combine the VPN label and the package name.
|
||||
*
|
||||
* If the VPN label violates the length restriction, the first 30 code points of VPN label and
|
||||
* the package name will be returned. Or return the VPN label and the package name directly if
|
||||
* the VPN label doesn't violate the length restriction.
|
||||
*
|
||||
* The result will be something like,
|
||||
* - ThisIsAVeryLongVpnAppNameWhich... (com.vpn.app)
|
||||
* if the VPN label violates the length restriction.
|
||||
* or
|
||||
* - VpnLabelWith<br>HtmlTag (com.vpn.app)
|
||||
* if the VPN label doesn't violate the length restriction.
|
||||
*
|
||||
*/
|
||||
private String getSimplifiedLabel(String vpnLabel, String packageName) {
|
||||
if (vpnLabel.codePointCount(0, vpnLabel.length()) > 30) {
|
||||
return getString(R.string.sanitized_vpn_label_with_ellipsis,
|
||||
vpnLabel.substring(0, vpnLabel.offsetByCodePoints(0, 30)),
|
||||
packageName);
|
||||
}
|
||||
|
||||
return getString(R.string.sanitized_vpn_label, vpnLabel, packageName);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected String getSanitizedVpnLabel(String vpnLabel, String packageName) {
|
||||
final String sanitizedVpnLabel = Html.escapeHtml(vpnLabel);
|
||||
final boolean exceedMaxVpnLabelLength = sanitizedVpnLabel.codePointCount(0,
|
||||
sanitizedVpnLabel.length()) > MAX_VPN_LABEL_LENGTH;
|
||||
if (exceedMaxVpnLabelLength || !vpnLabel.equals(sanitizedVpnLabel)) {
|
||||
return getSimplifiedLabel(sanitizedVpnLabel, packageName);
|
||||
}
|
||||
|
||||
return sanitizedVpnLabel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -75,15 +120,16 @@ public class ConfirmDialog extends AlertActivity
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
View view = View.inflate(this, R.layout.confirm, null);
|
||||
((TextView) view.findViewById(R.id.warning)).setText(
|
||||
Html.fromHtml(getString(R.string.warning, getVpnLabel()),
|
||||
this, null /* tagHandler */));
|
||||
mView = View.inflate(this, R.layout.confirm, null);
|
||||
((TextView) mView.findViewById(R.id.warning)).setText(
|
||||
Html.fromHtml(getString(R.string.warning, getSanitizedVpnLabel(
|
||||
getVpnLabel().toString(), mPackage)),
|
||||
this /* imageGetter */, null /* tagHandler */));
|
||||
mAlertParams.mTitle = getText(R.string.prompt);
|
||||
mAlertParams.mPositiveButtonText = getText(android.R.string.ok);
|
||||
mAlertParams.mPositiveButtonListener = this;
|
||||
mAlertParams.mNegativeButtonText = getText(android.R.string.cancel);
|
||||
mAlertParams.mView = view;
|
||||
mAlertParams.mView = mView;
|
||||
setupAlert();
|
||||
|
||||
getWindow().setCloseOnTouchOutside(false);
|
||||
@@ -92,6 +138,11 @@ public class ConfirmDialog extends AlertActivity
|
||||
button.setFilterTouchesWhenObscured(true);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public CharSequence getWarningText() {
|
||||
return ((TextView) mView.findViewById(R.id.warning)).getText();
|
||||
}
|
||||
|
||||
private CharSequence getVpnLabel() {
|
||||
try {
|
||||
return VpnConfig.getVpnLabel(this, mPackage);
|
||||
|
||||
36
packages/VpnDialogs/tests/Android.bp
Normal file
36
packages/VpnDialogs/tests/Android.bp
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 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 {
|
||||
default_applicable_licenses: ["frameworks_base_license"],
|
||||
}
|
||||
|
||||
android_test {
|
||||
name: "VpnDialogsTests",
|
||||
// Use platform certificate because the test will invoke a hidden API.
|
||||
// (e.g. VpnManager#prepareVpn()).
|
||||
certificate: "platform",
|
||||
libs: [
|
||||
"android.test.runner",
|
||||
"android.test.base",
|
||||
],
|
||||
static_libs: [
|
||||
"androidx.test.core",
|
||||
"androidx.test.rules",
|
||||
"androidx.test.ext.junit",
|
||||
"mockito-target-minus-junit4",
|
||||
"VpnDialogsLib",
|
||||
],
|
||||
srcs: ["src/**/*.java"],
|
||||
}
|
||||
31
packages/VpnDialogs/tests/AndroidManifest.xml
Normal file
31
packages/VpnDialogs/tests/AndroidManifest.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.android.vpndialogs.tests">
|
||||
|
||||
<application android:debuggable="true">
|
||||
<uses-library android:name="android.test.runner" />
|
||||
<activity android:name="com.android.vpndialogs.VpnDialogTest$InstrumentedConfirmDialog"/>
|
||||
</application>
|
||||
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
android:targetPackage="com.android.vpndialogs.tests"
|
||||
android:label="Vpn dialog tests">
|
||||
</instrumentation>
|
||||
</manifest>
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.vpndialogs;
|
||||
|
||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.VpnManager;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class VpnDialogTest {
|
||||
private ActivityScenario<ConfirmDialog> mActivityScenario;
|
||||
|
||||
@SuppressWarnings("StaticMockMember")
|
||||
@Mock
|
||||
private static PackageManager sPm;
|
||||
|
||||
@SuppressWarnings("StaticMockMember")
|
||||
@Mock
|
||||
private static VpnManager sVm;
|
||||
|
||||
@Mock
|
||||
private ApplicationInfo mAi;
|
||||
|
||||
private static final String VPN_APP_NAME = "VpnApp";
|
||||
private static final String VPN_APP_PACKAGE_NAME = "com.android.vpndialogs.VpnDialogTest";
|
||||
private static final String VPN_LABEL_CONTAINS_HTML_TAG =
|
||||
"<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a>";
|
||||
private static final String VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION =
|
||||
"<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a></b>"
|
||||
+ " Wants to connect the network. <br></br><br></br><br></br><br></br><br></br>"
|
||||
+ " <br></br><br></br><br></br><br></br><br></br><br></br><br></br><br></br> Deny it?";
|
||||
private static final String VPN_LABEL_VIOLATES_LENGTH_RESTRICTION = "This is a VPN label"
|
||||
+ " which violates the length restriction. The length restriction here are 150 code"
|
||||
+ " points. So the VPN label should be sanitized, and shows the package name to the"
|
||||
+ " user.";
|
||||
|
||||
public static class InstrumentedConfirmDialog extends ConfirmDialog {
|
||||
@Override
|
||||
public PackageManager getPackageManager() {
|
||||
return sPm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getSystemService(@ServiceName @NonNull String name) {
|
||||
switch (name) {
|
||||
case Context.VPN_MANAGEMENT_SERVICE:
|
||||
return sVm;
|
||||
default:
|
||||
return super.getSystemService(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCallingPackage() {
|
||||
return VPN_APP_PACKAGE_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
private void launchActivity() {
|
||||
final Context context = getInstrumentation().getContext();
|
||||
mActivityScenario = ActivityScenario.launch(
|
||||
new Intent(context, InstrumentedConfirmDialog.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSanitizedVpnLabel_withNormalCase() throws Exception {
|
||||
// Test the normal case that the VPN label showed in the VpnDialog is the app name.
|
||||
doReturn(VPN_APP_NAME).when(mAi).loadLabel(sPm);
|
||||
launchActivity();
|
||||
mActivityScenario.onActivity(activity -> {
|
||||
assertTrue(activity.getWarningText().toString().contains(VPN_APP_NAME));
|
||||
});
|
||||
}
|
||||
|
||||
private void verifySanitizedVpnLabel(String originalLabel) {
|
||||
doReturn(originalLabel).when(mAi).loadLabel(sPm);
|
||||
launchActivity();
|
||||
mActivityScenario.onActivity(activity -> {
|
||||
// The VPN label was sanitized because violating length restriction or having a html
|
||||
// tag, so the warning message will contain the package name.
|
||||
assertTrue(activity.getWarningText().toString().contains(activity.getCallingPackage()));
|
||||
// Also, the length of sanitized VPN label shouldn't longer than MAX_VPN_LABEL_LENGTH
|
||||
// and it shouldn't contain html tag.
|
||||
final String sanitizedVpnLabel =
|
||||
activity.getSanitizedVpnLabel(originalLabel, VPN_APP_PACKAGE_NAME);
|
||||
assertTrue(sanitizedVpnLabel.codePointCount(0, sanitizedVpnLabel.length())
|
||||
< ConfirmDialog.MAX_VPN_LABEL_LENGTH);
|
||||
assertFalse(sanitizedVpnLabel.contains("<b>"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSanitizedVpnLabel_withHtmlTag() throws Exception {
|
||||
// Test the case that the VPN label was sanitized because there is a html tag.
|
||||
verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSanitizedVpnLabel_withHtmlTagAndViolateLengthRestriction() throws Exception {
|
||||
// Test the case that the VPN label was sanitized because there is a html tag.
|
||||
verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSanitizedVpnLabel_withLengthRestriction() throws Exception {
|
||||
// Test the case that the VPN label was sanitized because hitting the length restriction.
|
||||
verifySanitizedVpnLabel(VPN_LABEL_VIOLATES_LENGTH_RESTRICTION);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
doReturn(false).when(sVm).prepareVpn(anyString(), anyString(), anyInt());
|
||||
doReturn(null).when(sPm).queryIntentServices(any(), anyInt());
|
||||
doReturn(mAi).when(sPm).getApplicationInfo(anyString(), anyInt());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user