Skip to content

Commit 94532a6

Browse files
authored
binder: Introduce server pre-authorization (#12127)
grpc-binder clients authorize servers by checking the UID of the sender of the SETUP_TRANSPORT Binder transaction against some SecurityPolicy. But merely binding to an unauthorized server to learn its UID can enable "keep-alive" and "background activity launch" abuse, even if security policy ultimately decides the connection is unauthorized. Pre-authorization mitigates this kind of abuse by looking up and authorizing a candidate server Application's UID before binding to it. Pre-auth is especially important when the server's address is not fixed in advance but discovered by PackageManager lookup.
1 parent 6dfa03c commit 94532a6

File tree

13 files changed

+598
-36
lines changed

13 files changed

+598
-36
lines changed

binder/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ dependencies {
7272
androidTestImplementation testFixtures(project(':grpc-core'))
7373

7474
testFixturesImplementation libraries.guava.testlib
75+
testFixturesImplementation testFixtures(project(':grpc-core'))
7576
}
7677

7778
import net.ltgt.gradle.errorprone.CheckSeverity

binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ public BinderClientTransportBuilder setReadyTimeoutMillis(int timeoutMillis) {
172172
return this;
173173
}
174174

175+
@CanIgnoreReturnValue
176+
public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeServer) {
177+
factoryBuilder.setPreAuthorizeServers(preAuthorizeServer);
178+
return this;
179+
}
180+
175181
public BinderTransport.BinderClientTransport build() {
176182
return factoryBuilder
177183
.buildClientTransportFactory()
@@ -372,11 +378,12 @@ public void testBlackHoleEndpointConnectTimeout() throws Exception {
372378
}
373379

374380
@Test
375-
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
381+
public void testBlackHoleSecurityPolicyAuthTimeout() throws Exception {
376382
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
377383
transport =
378384
new BinderClientTransportBuilder()
379385
.setSecurityPolicy(securityPolicy)
386+
.setPreAuthorizeServer(false)
380387
.setReadyTimeoutMillis(1_234)
381388
.build();
382389
transport.start(transportListener).run();
@@ -387,15 +394,39 @@ public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
387394
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
388395
assertThat(transportStatus.getDescription()).contains("1234");
389396
transportListener.awaitTermination();
390-
391397
// If the transport gave up waiting on auth, it should cancel its request.
392398
assertThat(authRequest.isCancelled()).isTrue();
393399
}
394400

395401
@Test
396-
public void testAsyncSecurityPolicyFailure() throws Exception {
402+
public void testBlackHoleSecurityPolicyPreAuthTimeout() throws Exception {
397403
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
398-
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
404+
transport =
405+
new BinderClientTransportBuilder()
406+
.setSecurityPolicy(securityPolicy)
407+
.setPreAuthorizeServer(true)
408+
.setReadyTimeoutMillis(1_234)
409+
.build();
410+
transport.start(transportListener).run();
411+
// Take the next authRequest but don't respond to it, in order to trigger the ready timeout.
412+
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
413+
414+
Status transportStatus = transportListener.awaitShutdown();
415+
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
416+
assertThat(transportStatus.getDescription()).contains("1234");
417+
transportListener.awaitTermination();
418+
// If the transport gave up waiting on auth, it should cancel its request.
419+
assertThat(preAuthRequest.isCancelled()).isTrue();
420+
}
421+
422+
@Test
423+
public void testAsyncSecurityPolicyAuthFailure() throws Exception {
424+
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
425+
transport =
426+
new BinderClientTransportBuilder()
427+
.setPreAuthorizeServer(false)
428+
.setSecurityPolicy(securityPolicy)
429+
.build();
399430
RuntimeException exception = new NullPointerException();
400431
transport.start(transportListener).run();
401432
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
@@ -406,15 +437,55 @@ public void testAsyncSecurityPolicyFailure() throws Exception {
406437
}
407438

408439
@Test
409-
public void testAsyncSecurityPolicySuccess() throws Exception {
440+
public void testAsyncSecurityPolicyPreAuthFailure() throws Exception {
410441
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
411-
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
442+
transport =
443+
new BinderClientTransportBuilder()
444+
.setPreAuthorizeServer(true)
445+
.setSecurityPolicy(securityPolicy)
446+
.build();
447+
RuntimeException exception = new NullPointerException();
448+
transport.start(transportListener).run();
449+
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
450+
Status transportStatus = transportListener.awaitShutdown();
451+
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
452+
assertThat(transportStatus.getCause()).isEqualTo(exception);
453+
transportListener.awaitTermination();
454+
}
455+
456+
@Test
457+
public void testAsyncSecurityPolicyAuthSuccess() throws Exception {
458+
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
459+
transport =
460+
new BinderClientTransportBuilder()
461+
.setPreAuthorizeServer(false)
462+
.setSecurityPolicy(securityPolicy)
463+
.build();
464+
transport.start(transportListener).run();
465+
securityPolicy
466+
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
467+
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
468+
Status transportStatus = transportListener.awaitShutdown();
469+
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
470+
assertThat(transportStatus.getDescription()).contains("xyzzy");
471+
transportListener.awaitTermination();
472+
}
473+
474+
@Test
475+
public void testAsyncSecurityPolicyPreAuthSuccess() throws Exception {
476+
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
477+
transport =
478+
new BinderClientTransportBuilder()
479+
.setPreAuthorizeServer(true)
480+
.setSecurityPolicy(securityPolicy)
481+
.build();
412482
transport.start(transportListener).run();
413483
securityPolicy
414484
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
415-
.setResult(Status.PERMISSION_DENIED);
485+
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
416486
Status transportStatus = transportListener.awaitShutdown();
417487
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
488+
assertThat(transportStatus.getDescription()).contains("xyzzy");
418489
transportListener.awaitTermination();
419490
}
420491

binder/src/main/java/io/grpc/binder/ApiConstants.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import android.content.Intent;
2020
import android.os.UserHandle;
21+
import io.grpc.Attributes;
22+
import io.grpc.EquivalentAddressGroup;
2123
import io.grpc.ExperimentalApi;
2224
import io.grpc.NameResolver;
2325

@@ -46,4 +48,16 @@ private ApiConstants() {}
4648
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
4749
public static final NameResolver.Args.Key<UserHandle> TARGET_ANDROID_USER =
4850
NameResolver.Args.Key.create("target-android-user");
51+
52+
/**
53+
* Lets you override a Channel's pre-auth configuration (see {@link
54+
* BinderChannelBuilder#preAuthorizeServers(boolean)}) for a given {@link EquivalentAddressGroup}.
55+
*
56+
* <p>A {@link NameResolver} that discovers servers from an untrusted source like PackageManager
57+
* can use this to force server pre-auth and prevent abuse.
58+
*/
59+
@EquivalentAddressGroup.Attr
60+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
61+
public static final Attributes.Key<Boolean> PRE_AUTH_SERVER_OVERRIDE =
62+
Attributes.Key.create("pre-auth-server-override");
4963
}

binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,35 @@ public BinderChannelBuilder strictLifecycleManagement() {
279279
return this;
280280
}
281281

282+
/**
283+
* Checks servers against this Channel's {@link SecurityPolicy} *before* binding.
284+
*
285+
* <p>Android users can be tricked into installing a malicious app with the same package name as a
286+
* legitimate server. That's why we don't send calls to a server until it has been authorized by
287+
* an appropriate {@link SecurityPolicy}. But merely binding to a malicious server can enable
288+
* "keep-alive" and "background activity launch" abuse, even if it's ultimately unauthorized.
289+
* Pre-authorization mitigates these threats by performing a preliminary {@link SecurityPolicy}
290+
* check against a server app's PackageManager-registered identity without actually creating an
291+
* instance of it. This is especially important for security when the server's direct address
292+
* isn't known in advance but rather resolved via target URI or discovered by other means.
293+
*
294+
* <p>Note that, unlike ordinary authorization, pre-authorization is performed against the server
295+
* app's UID, not the UID of the process hosting the bound Service. These can be different, most
296+
* commonly due to services that set `android:isolatedProcess=true`.
297+
*
298+
* <p>Pre-authorization is strongly recommended but it remains optional for now because of this
299+
* behavior change and the small performance cost.
300+
*
301+
* <p>The default value of this property is false but it will become true in a future release.
302+
* Clients that require a particular behavior should configure it explicitly using this method
303+
* rather than relying on the default.
304+
*/
305+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
306+
public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
307+
transportFactoryBuilder.setPreAuthorizeServers(preAuthorize);
308+
return this;
309+
}
310+
282311
@Override
283312
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
284313
checkState(

binder/src/main/java/io/grpc/binder/internal/Bindable.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package io.grpc.binder.internal;
1818

19+
import android.content.pm.ServiceInfo;
1920
import android.os.IBinder;
2021
import androidx.annotation.AnyThread;
2122
import androidx.annotation.MainThread;
2223
import io.grpc.Status;
24+
import io.grpc.StatusException;
2325

2426
/** An interface for managing a {@code Binder} connection. */
2527
interface Bindable {
@@ -45,6 +47,19 @@ interface Observer {
4547
void onUnbound(Status reason);
4648
}
4749

50+
/**
51+
* Fetches details about the remote Service from PackageManager without binding to it.
52+
*
53+
* <p>Resolving an untrusted address before binding to it lets you screen out problematic servers
54+
* before giving them a chance to run. However, note that the identity/existence of the resolved
55+
* Service can change between the time this method returns and the time you actually bind/connect
56+
* to it. For example, suppose the target package gets uninstalled or upgraded right after this
57+
* method returns. In {@link Observer#onBound}, you should verify that the server you resolved is
58+
* the same one you connected to.
59+
*/
60+
@AnyThread
61+
ServiceInfo resolve() throws StatusException;
62+
4863
/**
4964
* Attempt to bind with the remote service.
5065
*

binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
5555
final InboundParcelablePolicy inboundParcelablePolicy;
5656
final OneWayBinderProxy.Decorator binderDecorator;
5757
final long readyTimeoutMillis;
58+
final boolean preAuthorizeServers; // TODO(jdcormie): Default to true.
5859

5960
ScheduledExecutorService executorService;
6061
Executor offloadExecutor;
@@ -75,6 +76,7 @@ private BinderClientTransportFactory(Builder builder) {
7576
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
7677
binderDecorator = checkNotNull(builder.binderDecorator);
7778
readyTimeoutMillis = builder.readyTimeoutMillis;
79+
preAuthorizeServers = builder.preAuthorizeServers;
7880

7981
executorService = scheduledExecutorPool.getObject();
8082
offloadExecutor = offloadExecutorPool.getObject();
@@ -128,6 +130,7 @@ public static final class Builder implements ClientTransportFactoryBuilder {
128130
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
129131
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
130132
long readyTimeoutMillis = 60_000;
133+
boolean preAuthorizeServers;
131134

132135
@Override
133136
public BinderClientTransportFactory buildClientTransportFactory() {
@@ -216,5 +219,11 @@ public Builder setReadyTimeoutMillis(long readyTimeoutMillis) {
216219
this.readyTimeoutMillis = readyTimeoutMillis;
217220
return this;
218221
}
222+
223+
/** Whether to check server addresses against the SecurityPolicy *before* binding to them. */
224+
public Builder setPreAuthorizeServers(boolean preAuthorizeServers) {
225+
this.preAuthorizeServers = preAuthorizeServers;
226+
return this;
227+
}
219228
}
220229
}

0 commit comments

Comments
 (0)