Skip to content

Commit c6640bd

Browse files
committed
Refuse a cookie whose Domain names a public suffix
RFC 6265 section 5.3 implemented step 6 but not step 5, so evil.co.uk could set Domain=co.uk and every other host under that registry received the cookie. Bundle the ICANN section of the Mozilla list and reject a Domain that matches it, honouring its wildcard and exception rules. Label counting cannot stand in: co.uk has a dot like any ordinary domain. If the list cannot be read nothing is treated as a public suffix.
1 parent 735e0af commit c6640bd

4 files changed

Lines changed: 7150 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.asynchttpclient.cookie;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import java.io.BufferedReader;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.InputStreamReader;
25+
import java.util.Collections;
26+
import java.util.HashSet;
27+
import java.util.Set;
28+
29+
import static java.nio.charset.StandardCharsets.UTF_8;
30+
31+
/**
32+
* The ICANN section of the Mozilla Public Suffix List, used to decide whether a cookie {@code Domain}
33+
* attribute names a registry rather than a site.
34+
*
35+
* <p>RFC 6265 Section 5.3 step 5 requires rejecting a {@code Domain} that is a public suffix, and that
36+
* rule cannot be approximated: {@code co.uk} has a dot like any ordinary domain, so counting labels does
37+
* not distinguish a registry from a site. Without the list a host under {@code co.uk} can set a cookie for
38+
* {@code co.uk} itself and every other host under that suffix receives it.
39+
*
40+
* <p>Only the ICANN section is bundled. The private section describes organisations that let others
41+
* register names beneath them, which is a weaker property than a registry and not what step 5 is about.
42+
*
43+
* <p>The list is data and goes stale as registries change. A suffix added upstream after this release is
44+
* not recognised until the bundled copy is refreshed, so this narrows the exposure rather than closing it
45+
* for all time. If the resource cannot be read the check reports nothing as a public suffix, leaving
46+
* behaviour as it was rather than rejecting cookies that used to work.
47+
*/
48+
public final class PublicSuffixList {
49+
50+
private static final Logger LOGGER = LoggerFactory.getLogger(PublicSuffixList.class);
51+
private static final String RESOURCE = "/org/asynchttpclient/cookie/public_suffix_list.dat";
52+
53+
private static final Set<String> EXACT;
54+
private static final Set<String> WILDCARD;
55+
private static final Set<String> EXCEPTIONS;
56+
57+
static {
58+
Set<String> exact = new HashSet<>(8192);
59+
Set<String> wildcard = new HashSet<>(32);
60+
Set<String> exceptions = new HashSet<>(16);
61+
try (InputStream in = PublicSuffixList.class.getResourceAsStream(RESOURCE)) {
62+
if (in == null) {
63+
LOGGER.warn("Public suffix list {} is missing; a cookie Domain naming a public suffix "
64+
+ "cannot be rejected", RESOURCE);
65+
} else {
66+
BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8));
67+
String line;
68+
while ((line = reader.readLine()) != null) {
69+
String rule = line.trim();
70+
if (rule.isEmpty() || rule.startsWith("//")) {
71+
continue;
72+
}
73+
if (rule.charAt(0) == '!') {
74+
exceptions.add(rule.substring(1).toLowerCase());
75+
} else if (rule.startsWith("*.")) {
76+
wildcard.add(rule.substring(2).toLowerCase());
77+
} else {
78+
exact.add(rule.toLowerCase());
79+
}
80+
}
81+
}
82+
} catch (IOException e) {
83+
LOGGER.warn("Could not read the public suffix list; a cookie Domain naming a public suffix "
84+
+ "cannot be rejected", e);
85+
}
86+
EXACT = Collections.unmodifiableSet(exact);
87+
WILDCARD = Collections.unmodifiableSet(wildcard);
88+
EXCEPTIONS = Collections.unmodifiableSet(exceptions);
89+
}
90+
91+
private PublicSuffixList() {
92+
}
93+
94+
/**
95+
* Whether {@code domain} is a public suffix, and so may not be the {@code Domain} of a cookie.
96+
*
97+
* @param domain a hostname, without a leading dot
98+
*/
99+
public static boolean isPublicSuffix(String domain) {
100+
if (domain == null || domain.isEmpty()) {
101+
return false;
102+
}
103+
String candidate = domain.toLowerCase();
104+
if (candidate.charAt(candidate.length() - 1) == '.') {
105+
candidate = candidate.substring(0, candidate.length() - 1);
106+
}
107+
// An exception rule names something that IS registrable despite matching a wildcard above it.
108+
if (EXCEPTIONS.contains(candidate)) {
109+
return false;
110+
}
111+
if (EXACT.contains(candidate)) {
112+
return true;
113+
}
114+
// A wildcard rule such as *.ck makes every direct child of ck a suffix, so the candidate is one
115+
// when its parent carries the rule.
116+
int dot = candidate.indexOf('.');
117+
return dot > 0 && WILDCARD.contains(candidate.substring(dot + 1));
118+
}
119+
}

client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,15 @@ private void add(String requestDomain, String requestPath, Cookie cookie) {
201201
return;
202202
}
203203

204+
// rfc6265#section-5.3 step 5: ignore a cookie whose Domain names a public suffix. Step 6 above only
205+
// asks whether the request host sits under the Domain, which evil.co.uk setting Domain=co.uk
206+
// satisfies, so on its own it still lets one site plant a cookie every other site under that
207+
// registry receives. Label counting cannot stand in for this: co.uk has a dot like any other
208+
// domain.
209+
if (!hostOnly && PublicSuffixList.isPublicSuffix(keyDomain)) {
210+
return;
211+
}
212+
204213
String keyPath = cookiePath(cookie.path(), requestPath);
205214
CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyPath);
206215

0 commit comments

Comments
 (0)