Loading core/java/android/security/responsible_apis_flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -166,6 +166,13 @@ flag { bug: "295238578" } flag { name: "enable_intent_firewall_extra_key_value_filter" namespace: "responsible_apis" description: "Flag for enable ExtraKeyValueFilter in IntentFirewall" bug: "428733109" } flag { name: "fail_on_parcel_size_mismatch" namespace: "responsible_apis" Loading services/core/java/com/android/server/firewall/ExtraKeyValueFilter.java 0 → 100644 +89 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.server.firewall; import static android.security.Flags.enableIntentFirewallExtraKeyValueFilter; import android.content.ComponentName; import android.content.Intent; import android.os.Bundle; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; class ExtraKeyValueFilter implements Filter { private static final String ATTR_NAME = "name"; private final String mKeyFilter; private final StringFilter mValueFilter; private ExtraKeyValueFilter(String keyFilter, StringFilter valueFilter) { this.mKeyFilter = keyFilter; this.mValueFilter = valueFilter; } @Override public boolean matches(IntentFirewall ifw, ComponentName resolvedComponent, Intent intent, int callerUid, int callerPid, String resolvedType, int receivingUid) { if (!enableIntentFirewallExtraKeyValueFilter()) { return false; } Bundle extras = intent.getExtras(); if (extras == null) return false; return mValueFilter.matchesValue(extras.getString(mKeyFilter)); } public static final FilterFactory FACTORY = new FilterFactory("extra") { @Override public Filter newFilter(XmlPullParser parser) throws IOException, XmlPullParserException { String keyFilter = null; StringFilter valueFilter = null; final int depth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, depth)) { String elementName = parser.getName(); switch (elementName) { case "key" -> { if (keyFilter != null) { throw new XmlPullParserException( "Multiple key elements found in an extra element"); } keyFilter = parser.getAttributeValue(null, ATTR_NAME); } case "value" -> { if (valueFilter != null) { throw new XmlPullParserException( "Multiple value elements found in an extra element"); } valueFilter = StringFilter.readFromXml(null, parser); } default -> throw new XmlPullParserException( "Unknown element in extra rule: " + elementName); } } if (keyFilter == null || valueFilter == null) { throw new XmlPullParserException("<extra> must contain both <key> and <value>"); } return new ExtraKeyValueFilter(keyFilter, valueFilter); } }; } services/core/java/com/android/server/firewall/IntentFirewall.java +51 −22 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.server.firewall; import static android.security.Flags.enableIntentFirewallExtraKeyValueFilter; import android.annotation.NonNull; import android.app.AppGlobals; import android.content.ComponentName; Loading Loading @@ -87,7 +89,33 @@ public class IntentFirewall { private FirewallIntentResolver mServiceResolver = new FirewallIntentResolver(); static { FilterFactory[] factories = new FilterFactory[] { FilterFactory[] factories; if (enableIntentFirewallExtraKeyValueFilter()) { factories = new FilterFactory[]{ AndFilter.FACTORY, OrFilter.FACTORY, NotFilter.FACTORY, StringFilter.ACTION, StringFilter.COMPONENT, StringFilter.COMPONENT_NAME, StringFilter.COMPONENT_PACKAGE, StringFilter.DATA, StringFilter.HOST, StringFilter.MIME_TYPE, StringFilter.SCHEME, StringFilter.PATH, StringFilter.SSP, CategoryFilter.FACTORY, SenderFilter.FACTORY, SenderPackageFilter.FACTORY, SenderPermissionFilter.FACTORY, PortFilter.FACTORY, ExtraKeyValueFilter.FACTORY }; } else { factories = new FilterFactory[]{ AndFilter.FACTORY, OrFilter.FACTORY, NotFilter.FACTORY, Loading @@ -109,6 +137,7 @@ public class IntentFirewall { SenderPermissionFilter.FACTORY, PortFilter.FACTORY }; } // load factor ~= .75 factoryMap = new HashMap<String, FilterFactory>(factories.length * 4 / 3); Loading services/tests/servicestests/src/com/android/server/firewall/ExtraKeyValueFilterTest.java 0 → 100644 +139 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.server.firewall; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.Intent; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.StringReader; @RunWith(AndroidJUnit4.class) public class ExtraKeyValueFilterTest { private static final String EQUALS_KEY_FILTER_RULE = """ <extra> <key name="user_id"/> <value startsWith="admin"/> </extra> """; private ExtraKeyValueFilter parseExtraFilter(String xml) throws Exception { XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); parser.setInput(new StringReader(xml)); parser.nextTag(); // <extra> return (ExtraKeyValueFilter) ExtraKeyValueFilter.FACTORY.newFilter(parser); } @Test public void testKeyFilterMatches_valueFilterMatches_success() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id", "admin123"); boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertTrue("Parsed ExtraKeyValueFilter should match the intent", result); } @Test public void testKeyFilterMatches_valueFilterDoesNotMatch_fail() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id", "guest"); boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertFalse("Parsed ExtraKeyValueFilter should not match the intent", result); } @Test public void testKeyFilterDoesNotMatch_valueFilterMatches_fail() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id1", "admin2"); boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertFalse("Parsed ExtraKeyValueFilter should not match the intent", result); } @Test public void testKeyValueFilterMixMatch_fail() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id", "guest"); // matches key, but not value intent.putExtra("user_id_1", "admin123"); // matches value, but not key boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertFalse("Parsed ExtraKeyValueFilter should not match the intent", result); } @Test public void testKeyFilterMatches_valueFilterDoesNotMatchNonStringValue_fail() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id", 1); boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertFalse("Parsed ExtraKeyValueFilter should not match the intent", result); } @Test(expected = XmlPullParserException.class) public void testWrongElementInExtraFilter_parseExtraFilter_fail() throws Exception { String xml = """ <extra> <key name="user_id"/> <value startsWith="admin"/> <foo name="user_id"/> </extra> """; parseExtraFilter(xml); } @Test(expected = XmlPullParserException.class) public void testDuplicateKeyElementInExtraFilter_parseExtraFilter_fail() throws Exception { String xml = """ <extra> <key name="user_id"/> <value startsWith="admin"/> <key name="user_id2"/> </extra> """; parseExtraFilter(xml); } @Test(expected = XmlPullParserException.class) public void testMissingKeyElementInExtraFilter_parseExtraFilter_fail() throws Exception { String xml = """ <extra> <value startsWith="admin"/> </extra> """; parseExtraFilter(xml); } } Loading
core/java/android/security/responsible_apis_flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -166,6 +166,13 @@ flag { bug: "295238578" } flag { name: "enable_intent_firewall_extra_key_value_filter" namespace: "responsible_apis" description: "Flag for enable ExtraKeyValueFilter in IntentFirewall" bug: "428733109" } flag { name: "fail_on_parcel_size_mismatch" namespace: "responsible_apis" Loading
services/core/java/com/android/server/firewall/ExtraKeyValueFilter.java 0 → 100644 +89 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.server.firewall; import static android.security.Flags.enableIntentFirewallExtraKeyValueFilter; import android.content.ComponentName; import android.content.Intent; import android.os.Bundle; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; class ExtraKeyValueFilter implements Filter { private static final String ATTR_NAME = "name"; private final String mKeyFilter; private final StringFilter mValueFilter; private ExtraKeyValueFilter(String keyFilter, StringFilter valueFilter) { this.mKeyFilter = keyFilter; this.mValueFilter = valueFilter; } @Override public boolean matches(IntentFirewall ifw, ComponentName resolvedComponent, Intent intent, int callerUid, int callerPid, String resolvedType, int receivingUid) { if (!enableIntentFirewallExtraKeyValueFilter()) { return false; } Bundle extras = intent.getExtras(); if (extras == null) return false; return mValueFilter.matchesValue(extras.getString(mKeyFilter)); } public static final FilterFactory FACTORY = new FilterFactory("extra") { @Override public Filter newFilter(XmlPullParser parser) throws IOException, XmlPullParserException { String keyFilter = null; StringFilter valueFilter = null; final int depth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, depth)) { String elementName = parser.getName(); switch (elementName) { case "key" -> { if (keyFilter != null) { throw new XmlPullParserException( "Multiple key elements found in an extra element"); } keyFilter = parser.getAttributeValue(null, ATTR_NAME); } case "value" -> { if (valueFilter != null) { throw new XmlPullParserException( "Multiple value elements found in an extra element"); } valueFilter = StringFilter.readFromXml(null, parser); } default -> throw new XmlPullParserException( "Unknown element in extra rule: " + elementName); } } if (keyFilter == null || valueFilter == null) { throw new XmlPullParserException("<extra> must contain both <key> and <value>"); } return new ExtraKeyValueFilter(keyFilter, valueFilter); } }; }
services/core/java/com/android/server/firewall/IntentFirewall.java +51 −22 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.server.firewall; import static android.security.Flags.enableIntentFirewallExtraKeyValueFilter; import android.annotation.NonNull; import android.app.AppGlobals; import android.content.ComponentName; Loading Loading @@ -87,7 +89,33 @@ public class IntentFirewall { private FirewallIntentResolver mServiceResolver = new FirewallIntentResolver(); static { FilterFactory[] factories = new FilterFactory[] { FilterFactory[] factories; if (enableIntentFirewallExtraKeyValueFilter()) { factories = new FilterFactory[]{ AndFilter.FACTORY, OrFilter.FACTORY, NotFilter.FACTORY, StringFilter.ACTION, StringFilter.COMPONENT, StringFilter.COMPONENT_NAME, StringFilter.COMPONENT_PACKAGE, StringFilter.DATA, StringFilter.HOST, StringFilter.MIME_TYPE, StringFilter.SCHEME, StringFilter.PATH, StringFilter.SSP, CategoryFilter.FACTORY, SenderFilter.FACTORY, SenderPackageFilter.FACTORY, SenderPermissionFilter.FACTORY, PortFilter.FACTORY, ExtraKeyValueFilter.FACTORY }; } else { factories = new FilterFactory[]{ AndFilter.FACTORY, OrFilter.FACTORY, NotFilter.FACTORY, Loading @@ -109,6 +137,7 @@ public class IntentFirewall { SenderPermissionFilter.FACTORY, PortFilter.FACTORY }; } // load factor ~= .75 factoryMap = new HashMap<String, FilterFactory>(factories.length * 4 / 3); Loading
services/tests/servicestests/src/com/android/server/firewall/ExtraKeyValueFilterTest.java 0 → 100644 +139 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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.server.firewall; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.Intent; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.StringReader; @RunWith(AndroidJUnit4.class) public class ExtraKeyValueFilterTest { private static final String EQUALS_KEY_FILTER_RULE = """ <extra> <key name="user_id"/> <value startsWith="admin"/> </extra> """; private ExtraKeyValueFilter parseExtraFilter(String xml) throws Exception { XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); parser.setInput(new StringReader(xml)); parser.nextTag(); // <extra> return (ExtraKeyValueFilter) ExtraKeyValueFilter.FACTORY.newFilter(parser); } @Test public void testKeyFilterMatches_valueFilterMatches_success() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id", "admin123"); boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertTrue("Parsed ExtraKeyValueFilter should match the intent", result); } @Test public void testKeyFilterMatches_valueFilterDoesNotMatch_fail() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id", "guest"); boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertFalse("Parsed ExtraKeyValueFilter should not match the intent", result); } @Test public void testKeyFilterDoesNotMatch_valueFilterMatches_fail() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id1", "admin2"); boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertFalse("Parsed ExtraKeyValueFilter should not match the intent", result); } @Test public void testKeyValueFilterMixMatch_fail() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id", "guest"); // matches key, but not value intent.putExtra("user_id_1", "admin123"); // matches value, but not key boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertFalse("Parsed ExtraKeyValueFilter should not match the intent", result); } @Test public void testKeyFilterMatches_valueFilterDoesNotMatchNonStringValue_fail() throws Exception { ExtraKeyValueFilter filter = parseExtraFilter(EQUALS_KEY_FILTER_RULE); Intent intent = new Intent(); intent.putExtra("user_id", 1); boolean result = filter.matches(null, null, intent, 0, 0, null, 0); assertFalse("Parsed ExtraKeyValueFilter should not match the intent", result); } @Test(expected = XmlPullParserException.class) public void testWrongElementInExtraFilter_parseExtraFilter_fail() throws Exception { String xml = """ <extra> <key name="user_id"/> <value startsWith="admin"/> <foo name="user_id"/> </extra> """; parseExtraFilter(xml); } @Test(expected = XmlPullParserException.class) public void testDuplicateKeyElementInExtraFilter_parseExtraFilter_fail() throws Exception { String xml = """ <extra> <key name="user_id"/> <value startsWith="admin"/> <key name="user_id2"/> </extra> """; parseExtraFilter(xml); } @Test(expected = XmlPullParserException.class) public void testMissingKeyElementInExtraFilter_parseExtraFilter_fail() throws Exception { String xml = """ <extra> <value startsWith="admin"/> </extra> """; parseExtraFilter(xml); } }