Skip to content

Commit eaf5028

Browse files
authored
Fix/safelist deep copy constructor (#1763)
Copies nested data structures instead of using a shallow copy to avoid unexpected state mutations after copy constructor usage.
1 parent 7261248 commit eaf5028

2 files changed

Lines changed: 83 additions & 8 deletions

File tree

src/main/java/org/jsoup/safety/Safelist.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ XSS attack examples (that jsoup will safegaurd against the default Cleaner and S
6363
</p>
6464
*/
6565
public class Safelist {
66-
private Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span]
67-
private Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag.
68-
private Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values
69-
private Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes
66+
private final Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span]
67+
private final Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag.
68+
private final Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values
69+
private final Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes
7070
private boolean preserveRelativeLinks; // option to preserve relative links
7171

7272
/**
@@ -203,9 +203,19 @@ public Safelist() {
203203
public Safelist(Safelist copy) {
204204
this();
205205
tagNames.addAll(copy.tagNames);
206-
attributes.putAll(copy.attributes);
207-
enforcedAttributes.putAll(copy.enforcedAttributes);
208-
protocols.putAll(copy.protocols);
206+
for (Map.Entry<TagName, Set<AttributeKey>> copyTagAttributes : copy.attributes.entrySet()) {
207+
attributes.put(copyTagAttributes.getKey(), new HashSet<>(copyTagAttributes.getValue()));
208+
}
209+
for (Map.Entry<TagName, Map<AttributeKey, AttributeValue>> enforcedEntry : copy.enforcedAttributes.entrySet()) {
210+
enforcedAttributes.put(enforcedEntry.getKey(), new HashMap<>(enforcedEntry.getValue()));
211+
}
212+
for (Map.Entry<TagName, Map<AttributeKey, Set<Protocol>>> protocolsEntry : copy.protocols.entrySet()) {
213+
Map<AttributeKey, Set<Protocol>> attributeProtocolsCopy = new HashMap<>();
214+
for (Map.Entry<AttributeKey, Set<Protocol>> attributeProtocols : protocolsEntry.getValue().entrySet()) {
215+
attributeProtocolsCopy.put(attributeProtocols.getKey(), new HashSet<>(attributeProtocols.getValue()));
216+
}
217+
protocols.put(protocolsEntry.getKey(), attributeProtocolsCopy);
218+
}
209219
preserveRelativeLinks = copy.preserveRelativeLinks;
210220
}
211221

@@ -620,7 +630,7 @@ static Protocol valueOf(String value) {
620630
}
621631

622632
abstract static class TypedValue {
623-
private String value;
633+
private final String value;
624634

625635
TypedValue(String value) {
626636
Validate.notNull(value);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.jsoup.safety;
2+
3+
import org.jsoup.nodes.Attribute;
4+
import org.jsoup.nodes.Attributes;
5+
import org.jsoup.nodes.Element;
6+
import org.jsoup.parser.Tag;
7+
import org.junit.jupiter.api.Test;
8+
9+
import static org.junit.jupiter.api.Assertions.assertFalse;
10+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
11+
12+
public class SafelistTest {
13+
private static final String TEST_TAG = "testTag";
14+
private static final String TEST_ATTRIBUTE = "testAttribute";
15+
private static final String TEST_SCHEME = "valid-scheme";
16+
private static final String TEST_VALUE = TEST_SCHEME + "://testValue";
17+
18+
@Test
19+
public void testCopyConstructor_noSideEffectOnTags() {
20+
Safelist safelist1 = Safelist.none().addTags(TEST_TAG);
21+
Safelist safelist2 = new Safelist(safelist1);
22+
safelist1.addTags("invalidTag");
23+
24+
assertFalse(safelist2.isSafeTag("invalidTag"));
25+
}
26+
27+
@Test
28+
public void testCopyConstructor_noSideEffectOnAttributes() {
29+
Safelist safelist1 = Safelist.none().addAttributes(TEST_TAG, TEST_ATTRIBUTE);
30+
Safelist safelist2 = new Safelist(safelist1);
31+
safelist1.addAttributes(TEST_TAG, "invalidAttribute");
32+
33+
assertFalse(safelist2.isSafeAttribute(TEST_TAG, null, new Attribute("invalidAttribute", TEST_VALUE)));
34+
}
35+
36+
@Test
37+
public void testCopyConstructor_noSideEffectOnEnforcedAttributes() {
38+
Safelist safelist1 = Safelist.none().addEnforcedAttribute(TEST_TAG, TEST_ATTRIBUTE, TEST_VALUE);
39+
Safelist safelist2 = new Safelist(safelist1);
40+
safelist1.addEnforcedAttribute(TEST_TAG, TEST_ATTRIBUTE, "invalidValue");
41+
42+
for (Attribute enforcedAttribute : safelist2.getEnforcedAttributes(TEST_TAG)) {
43+
assertNotEquals("invalidValue", enforcedAttribute.getValue());
44+
}
45+
}
46+
47+
@Test
48+
public void testCopyConstructor_noSideEffectOnProtocols() {
49+
final String invalidScheme = "invalid-scheme";
50+
Safelist safelist1 = Safelist.none()
51+
.addAttributes(TEST_TAG, TEST_ATTRIBUTE)
52+
.addProtocols(TEST_TAG, TEST_ATTRIBUTE, TEST_SCHEME);
53+
Safelist safelist2 = new Safelist(safelist1);
54+
safelist1.addProtocols(TEST_TAG, TEST_ATTRIBUTE, invalidScheme);
55+
56+
Attributes attributes = new Attributes();
57+
Attribute invalidAttribute = new Attribute(TEST_ATTRIBUTE, invalidScheme + "://someValue");
58+
attributes.put(invalidAttribute);
59+
Element invalidElement = new Element(Tag.valueOf(TEST_TAG), "", attributes);
60+
61+
assertFalse(safelist2.isSafeAttribute(TEST_TAG, invalidElement, invalidAttribute));
62+
}
63+
64+
65+
}

0 commit comments

Comments
 (0)