Skip to content

Commit a75bbdd

Browse files
authored
Prevent DoS attacks by rejecting unknown realms (#594)
1 parent 6744d28 commit a75bbdd

File tree

9 files changed

+132
-60
lines changed

9 files changed

+132
-60
lines changed

quarkus/service/src/main/resources/application.properties

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ quarkus.otel.sdk.disabled=false
7272
# quarkus.otel.traces.sampler=parentbased_always_on
7373
# quarkus.otel.traces.sampler.arg=1.0d
7474

75-
polaris.realm-context.default-realm=default-realm
7675
polaris.realm-context.type=default
76+
polaris.realm-context.realms=realm1,realm2,realm3
77+
polaris.realm-context.header-name=Polaris-Realm
7778

7879
polaris.features.defaults."ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING"=false
7980
polaris.features.defaults."SUPPORTED_CATALOG_STORAGE_TYPES"=["S3","GCS","AZURE","FILE"]
@@ -85,7 +86,7 @@ polaris.persistence.type=in-memory
8586

8687
polaris.file-io.type=default
8788

88-
polaris.log.request-id-header-name=request_id
89+
polaris.log.request-id-header-name=Polaris-Request-Id
8990
# polaris.log.mdc.aid=polaris
9091
# polaris.log.mdc.sid=polaris-service
9192

@@ -138,7 +139,8 @@ polaris.authentication.token-broker.max-token-generation=PT1H
138139
%test.polaris.features.defaults."INITIALIZE_DEFAULT_CATALOG_FILEIO_FOR_TEST"=true
139140
%test.polaris.features.defaults."SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION"=true
140141
%test.polaris.features.defaults."SUPPORTED_CATALOG_STORAGE_TYPES"=["FILE","S3","GCS","AZURE"]
141-
%test.polaris.realm-context.default-realm=POLARIS
142+
%test.polaris.realm-context.realms=POLARIS
143+
%test.polaris.realm-context.type=test
142144
%test.polaris.storage.aws.access-key=accessKey
143145
%test.polaris.storage.aws.secret-key=secretKey
144146
%test.polaris.storage.gcp.token=token

quarkus/service/src/test/java/org/apache/polaris/service/quarkus/TimedApplicationEventListenerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
package org.apache.polaris.service.quarkus;
2020

21-
import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY;
21+
import static org.apache.polaris.service.context.TestRealmContextResolver.REALM_PROPERTY_KEY;
2222
import static org.assertj.core.api.Assertions.assertThat;
2323
import static org.assertj.core.api.InstanceOfAssertFactories.type;
2424

quarkus/service/src/test/java/org/apache/polaris/service/quarkus/auth/TokenUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
package org.apache.polaris.service.quarkus.auth;
2020

2121
import static org.apache.polaris.service.auth.BasePolarisAuthenticator.PRINCIPAL_ROLE_ALL;
22-
import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY;
22+
import static org.apache.polaris.service.context.TestRealmContextResolver.REALM_PROPERTY_KEY;
2323
import static org.assertj.core.api.Assertions.assertThat;
2424

2525
import jakarta.ws.rs.client.Client;

quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/TestUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
package org.apache.polaris.service.quarkus.catalog;
2020

21-
import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY;
21+
import static org.apache.polaris.service.context.TestRealmContextResolver.REALM_PROPERTY_KEY;
2222
import static org.assertj.core.api.Assertions.assertThat;
2323

2424
import com.google.common.collect.ImmutableMap;

quarkus/service/src/test/java/org/apache/polaris/service/quarkus/ratelimiter/TestUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
package org.apache.polaris.service.quarkus.ratelimiter;
2020

21-
import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY;
21+
import static org.apache.polaris.service.context.TestRealmContextResolver.REALM_PROPERTY_KEY;
2222
import static org.assertj.core.api.Assertions.assertThat;
2323

2424
import jakarta.ws.rs.core.Response;

quarkus/service/src/test/java/org/apache/polaris/service/quarkus/test/PolarisIntegrationTestFixture.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919
package org.apache.polaris.service.quarkus.test;
2020

21-
import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY;
21+
import static org.apache.polaris.service.context.TestRealmContextResolver.REALM_PROPERTY_KEY;
2222
import static org.assertj.core.api.Assertions.assertThat;
2323

2424
import com.fasterxml.jackson.core.JsonProcessingException;

service/common/src/main/java/org/apache/polaris/service/context/DefaultRealmContextResolver.java

Lines changed: 9 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,15 @@
1818
*/
1919
package org.apache.polaris.service.context;
2020

21-
import com.google.common.base.Splitter;
2221
import io.smallrye.common.annotation.Identifier;
2322
import jakarta.enterprise.context.ApplicationScoped;
2423
import jakarta.inject.Inject;
25-
import java.util.HashMap;
2624
import java.util.Map;
2725
import org.apache.polaris.core.context.RealmContext;
28-
import org.slf4j.Logger;
29-
import org.slf4j.LoggerFactory;
3026

31-
/**
32-
* For local/dev testing, this resolver simply expects a custom bearer-token format that is a
33-
* semicolon-separated list of colon-separated key/value pairs that constitute the realm properties.
34-
*
35-
* <p>Example: principal:data-engineer;password:test;realm:acct123
36-
*/
3727
@ApplicationScoped
3828
@Identifier("default")
3929
public class DefaultRealmContextResolver implements RealmContextResolver {
40-
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRealmContextResolver.class);
41-
42-
public static final String REALM_PROPERTY_KEY = "realm";
4330

4431
private final RealmContextConfiguration configuration;
4532

@@ -51,47 +38,18 @@ public DefaultRealmContextResolver(RealmContextConfiguration configuration) {
5138
@Override
5239
public RealmContext resolveRealmContext(
5340
String requestURL, String method, String path, Map<String, String> headers) {
54-
// Since this default resolver is strictly for use in test/dev environments, we'll consider
55-
// it safe to log all contents. Any "real" resolver used in a prod environment should make
56-
// sure to only log non-sensitive contents.
57-
LOGGER.debug(
58-
"Resolving RealmContext for method: {}, path: {}, headers: {}", method, path, headers);
59-
Map<String, String> parsedProperties = parseBearerTokenAsKvPairs(headers);
60-
61-
if (!parsedProperties.containsKey(REALM_PROPERTY_KEY)
62-
&& headers.containsKey(REALM_PROPERTY_KEY)) {
63-
parsedProperties.put(REALM_PROPERTY_KEY, headers.get(REALM_PROPERTY_KEY));
64-
}
6541

66-
if (!parsedProperties.containsKey(REALM_PROPERTY_KEY)) {
67-
LOGGER.warn(
68-
"Failed to parse {} from headers; using {}",
69-
REALM_PROPERTY_KEY,
70-
configuration.defaultRealm());
71-
parsedProperties.put(REALM_PROPERTY_KEY, configuration.defaultRealm());
72-
}
73-
String realmId = parsedProperties.get(REALM_PROPERTY_KEY);
74-
return () -> realmId;
75-
}
42+
String realm;
7643

77-
/**
78-
* Returns kv pairs parsed from the "Authorization: Bearer k1:v1;k2:v2;k3:v3" header if it exists;
79-
* if missing, returns empty map.
80-
*/
81-
static Map<String, String> parseBearerTokenAsKvPairs(Map<String, String> headers) {
82-
Map<String, String> parsedProperties = new HashMap<>();
83-
if (headers != null) {
84-
String authHeader = headers.get("Authorization");
85-
if (authHeader != null) {
86-
String[] parts = authHeader.split(" ");
87-
if (parts.length == 2 && "Bearer".equalsIgnoreCase(parts[0])) {
88-
if (parts[1].matches("[\\w\\d=_+-]+:[\\w\\d=+_-]+(?:;[\\w\\d=+_-]+:[\\w\\d=+_-]+)*")) {
89-
parsedProperties.putAll(
90-
Splitter.on(';').trimResults().withKeyValueSeparator(':').split(parts[1]));
91-
}
92-
}
44+
if (headers.containsKey(configuration.headerName())) {
45+
realm = headers.get(configuration.headerName());
46+
if (!configuration.realms().contains(realm)) {
47+
throw new IllegalArgumentException("Unknown realm: " + realm);
9348
}
49+
} else {
50+
realm = configuration.defaultRealm();
9451
}
95-
return parsedProperties;
52+
53+
return () -> realm;
9654
}
9755
}

service/common/src/main/java/org/apache/polaris/service/context/RealmContextConfiguration.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,23 @@
1818
*/
1919
package org.apache.polaris.service.context;
2020

21+
import jakarta.validation.constraints.Size;
22+
import java.util.Set;
23+
2124
public interface RealmContextConfiguration {
2225

26+
/**
27+
* The set of realms that are supported by the realm context resolver. The first realm is
28+
* considered the default realm.
29+
*/
30+
@Size(min = 1)
31+
Set<String> realms();
32+
33+
/** The header name that contains the realm identifier. */
34+
String headerName();
35+
2336
/** The default realm to use when no realm is specified. */
24-
String defaultRealm();
37+
default String defaultRealm() {
38+
return realms().iterator().next();
39+
}
2540
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.service.context;
20+
21+
import com.google.common.base.Splitter;
22+
import io.smallrye.common.annotation.Identifier;
23+
import jakarta.enterprise.context.ApplicationScoped;
24+
import jakarta.inject.Inject;
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
import org.apache.polaris.core.context.RealmContext;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
/**
32+
* For local/dev testing, this resolver simply expects a custom bearer-token format that is a
33+
* semicolon-separated list of colon-separated key/value pairs that constitute the realm properties.
34+
*
35+
* <p>Example: principal:data-engineer;password:test;realm:acct123
36+
*/
37+
@ApplicationScoped
38+
@Identifier("test")
39+
public class TestRealmContextResolver implements RealmContextResolver {
40+
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRealmContextResolver.class);
41+
42+
public static final String REALM_PROPERTY_KEY = "realm";
43+
44+
private final RealmContextConfiguration configuration;
45+
46+
@Inject
47+
public TestRealmContextResolver(RealmContextConfiguration configuration) {
48+
this.configuration = configuration;
49+
}
50+
51+
@Override
52+
public RealmContext resolveRealmContext(
53+
String requestURL, String method, String path, Map<String, String> headers) {
54+
// Since this default resolver is strictly for use in test/dev environments, we'll consider
55+
// it safe to log all contents. Any "real" resolver used in a prod environment should make
56+
// sure to only log non-sensitive contents.
57+
LOGGER.debug(
58+
"Resolving RealmContext for method: {}, path: {}, headers: {}", method, path, headers);
59+
Map<String, String> parsedProperties = parseBearerTokenAsKvPairs(headers);
60+
61+
if (!parsedProperties.containsKey(REALM_PROPERTY_KEY)
62+
&& headers.containsKey(REALM_PROPERTY_KEY)) {
63+
parsedProperties.put(REALM_PROPERTY_KEY, headers.get(REALM_PROPERTY_KEY));
64+
}
65+
66+
if (!parsedProperties.containsKey(REALM_PROPERTY_KEY)) {
67+
LOGGER.warn(
68+
"Failed to parse {} from headers; using {}",
69+
REALM_PROPERTY_KEY,
70+
configuration.defaultRealm());
71+
parsedProperties.put(REALM_PROPERTY_KEY, configuration.defaultRealm());
72+
}
73+
String realmId = parsedProperties.get(REALM_PROPERTY_KEY);
74+
return () -> realmId;
75+
}
76+
77+
/**
78+
* Returns kv pairs parsed from the "Authorization: Bearer k1:v1;k2:v2;k3:v3" header if it exists;
79+
* if missing, returns empty map.
80+
*/
81+
private static Map<String, String> parseBearerTokenAsKvPairs(Map<String, String> headers) {
82+
Map<String, String> parsedProperties = new HashMap<>();
83+
if (headers != null) {
84+
String authHeader = headers.get("Authorization");
85+
if (authHeader != null) {
86+
String[] parts = authHeader.split(" ");
87+
if (parts.length == 2 && "Bearer".equalsIgnoreCase(parts[0])) {
88+
if (parts[1].matches("[\\w\\d=_+-]+:[\\w\\d=+_-]+(?:;[\\w\\d=+_-]+:[\\w\\d=+_-]+)*")) {
89+
parsedProperties.putAll(
90+
Splitter.on(';').trimResults().withKeyValueSeparator(':').split(parts[1]));
91+
}
92+
}
93+
}
94+
}
95+
return parsedProperties;
96+
}
97+
}

0 commit comments

Comments
 (0)