Skip to content

Commit f6381ab

Browse files
committed
Missing tests
1 parent c60bf63 commit f6381ab

3 files changed

Lines changed: 351 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.xml.security.utils.resolver;
20+
21+
/**
22+
* Shared utility methods for resource resolver implementations.
23+
*/
24+
public final class ResolverUtils {
25+
26+
private ResolverUtils() {
27+
}
28+
29+
/**
30+
* Returns {@code true} if {@code uri} carries an explicit URI scheme other than
31+
* {@code file:}. Relative URIs (no scheme), same-document references (fragment-only),
32+
* and {@code file:} URIs all return {@code false}.
33+
*
34+
* <p>The scheme is recognised according to RFC 3986:
35+
* {@code scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )}.
36+
* A string whose prefix before the first {@code ':'} does not conform to that grammar
37+
* is treated as a relative URI (returns {@code false}).</p>
38+
*
39+
* @param uri the URI string to test; may be {@code null}
40+
* @return {@code true} if {@code uri} has a non-{@code file:} scheme
41+
*/
42+
public static boolean hasExplicitNonFileScheme(String uri) {
43+
if (uri == null || uri.isEmpty()) {
44+
return false;
45+
}
46+
int colon = uri.indexOf(':');
47+
if (colon <= 0) {
48+
return false; // no scheme present — relative URI or fragment
49+
}
50+
// Validate that every character before ':' is a legal scheme character
51+
// (RFC 3986: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ))
52+
for (int i = 0; i < colon; i++) {
53+
char c = uri.charAt(i);
54+
boolean valid = (i == 0) ? Character.isLetter(c)
55+
: (Character.isLetterOrDigit(c) || c == '+' || c == '-' || c == '.');
56+
if (!valid) {
57+
return false; // not a valid scheme — treat as relative
58+
}
59+
}
60+
return !uri.startsWith("file:");
61+
}
62+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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.xml.security.test.dom.utils.resolver;
20+
21+
import org.apache.xml.security.test.dom.TestUtils;
22+
import org.apache.xml.security.utils.resolver.ResourceResolverContext;
23+
import org.apache.xml.security.utils.resolver.implementations.ResolverLocalFilesystem;
24+
import org.junit.jupiter.api.Test;
25+
import org.w3c.dom.Attr;
26+
import org.w3c.dom.Document;
27+
28+
import static org.junit.jupiter.api.Assertions.assertFalse;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
31+
/**
32+
* Tests for the scheme-checking logic in ResolverLocalFilesystem.
33+
*
34+
* The fix requires that at least one of uriToResolve / baseUri starts with "file:"
35+
* AND neither carries a different explicit scheme. This prevents a resolver-hijacking
36+
* attack where an https: (or ftp:, etc.) uriToResolve was incorrectly accepted solely
37+
* because the baseUri happened to start with "file:".
38+
*/
39+
class ResolverLocalFilesystemTest {
40+
41+
static {
42+
org.apache.xml.security.Init.init();
43+
}
44+
45+
private static ResourceResolverContext makeContext(String uri, String baseUri) throws Exception {
46+
Document doc = TestUtils.newDocument();
47+
Attr attr = doc.createAttribute("URI");
48+
attr.setValue(uri);
49+
return new ResourceResolverContext(attr, baseUri, false);
50+
}
51+
52+
/**
53+
* Baseline: a plain file: URI with a file: baseUri is accepted (expected behaviour).
54+
*/
55+
@Test
56+
void testFileUriWithFileBaseUriIsAccepted() throws Exception {
57+
ResolverLocalFilesystem resolver = new ResolverLocalFilesystem();
58+
ResourceResolverContext ctx = makeContext("file:///etc/hosts", "file:///var/app/");
59+
assertTrue(resolver.engineCanResolveURI(ctx),
60+
"A file: URI with a file: baseUri should be accepted");
61+
}
62+
63+
/**
64+
* FIX: an https: uriToResolve must be rejected even when baseUri is "file:".
65+
* Previously the resolver incorrectly claimed ownership of https: URIs solely
66+
* because baseUri started with "file:", allowing resolver hijacking.
67+
*/
68+
@Test
69+
void testHttpsUriIsRejectedWhenBaseUriIsFile() throws Exception {
70+
ResolverLocalFilesystem resolver = new ResolverLocalFilesystem();
71+
ResourceResolverContext ctx = makeContext("https://attacker.com/payload", "file:///var/app/");
72+
73+
assertFalse(resolver.engineCanResolveURI(ctx),
74+
"FIX VERIFIED: https: uriToResolve must be rejected even with a file: baseUri");
75+
}
76+
77+
/**
78+
* FIX: any non-file explicit scheme in uriToResolve must be rejected,
79+
* regardless of baseUri.
80+
*/
81+
@Test
82+
void testFtpUriIsRejectedWhenBaseUriIsFile() throws Exception {
83+
ResolverLocalFilesystem resolver = new ResolverLocalFilesystem();
84+
ResourceResolverContext ctx = makeContext("ftp://attacker.com/file.xml", "file:///var/app/");
85+
86+
assertFalse(resolver.engineCanResolveURI(ctx),
87+
"FIX VERIFIED: ftp: uriToResolve must be rejected even with a file: baseUri");
88+
}
89+
90+
/**
91+
* FIX: http: uriToResolve must also be rejected when baseUri is "file:".
92+
*/
93+
@Test
94+
void testHttpUriIsRejectedWhenBaseUriIsFile() throws Exception {
95+
ResolverLocalFilesystem resolver = new ResolverLocalFilesystem();
96+
ResourceResolverContext ctx = makeContext("http://attacker.com/payload", "file:///var/app/");
97+
98+
assertFalse(resolver.engineCanResolveURI(ctx),
99+
"FIX VERIFIED: http: uriToResolve must be rejected even with a file: baseUri");
100+
}
101+
102+
/**
103+
* A relative uriToResolve (no scheme) combined with a file: baseUri is accepted —
104+
* this is the primary legitimate use case for providing a file: baseUri.
105+
*/
106+
@Test
107+
void testRelativeUriWithFileBaseUriIsAccepted() throws Exception {
108+
ResolverLocalFilesystem resolver = new ResolverLocalFilesystem();
109+
ResourceResolverContext ctx = makeContext("subdoc.xml", "file:///var/app/");
110+
111+
assertTrue(resolver.engineCanResolveURI(ctx),
112+
"A relative uriToResolve with a file: baseUri must be accepted");
113+
}
114+
115+
/**
116+
* Sanity check: an https: URI with no baseUri (or a non-file: baseUri) is
117+
* correctly rejected by engineCanResolveURI.
118+
*/
119+
@Test
120+
void testHttpsUriWithNullBaseUriIsRejected() throws Exception {
121+
ResolverLocalFilesystem resolver = new ResolverLocalFilesystem();
122+
ResourceResolverContext ctx = makeContext("https://attacker.com/payload", null);
123+
124+
assertFalse(resolver.engineCanResolveURI(ctx),
125+
"https: URI with no baseUri should be rejected by the filesystem resolver");
126+
}
127+
128+
/**
129+
* Sanity check: an https: URI with an https: baseUri is also correctly rejected.
130+
*/
131+
@Test
132+
void testHttpsUriWithHttpsBaseUriIsRejected() throws Exception {
133+
ResolverLocalFilesystem resolver = new ResolverLocalFilesystem();
134+
ResourceResolverContext ctx = makeContext("https://attacker.com/payload", "https://victim.com/");
135+
136+
assertFalse(resolver.engineCanResolveURI(ctx),
137+
"https: URI with https: baseUri should be rejected by the filesystem resolver");
138+
}
139+
140+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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.xml.security.test.stax.resourceResolvers;
20+
21+
import org.apache.xml.security.stax.impl.resourceResolvers.ResolverFilesystem;
22+
import org.junit.jupiter.api.Test;
23+
24+
import static org.junit.jupiter.api.Assertions.assertNotNull;
25+
import static org.junit.jupiter.api.Assertions.assertNull;
26+
import static org.junit.jupiter.api.Assertions.assertFalse;
27+
28+
/**
29+
* Tests for the scheme-checking logic in ResolverFilesystem.
30+
*
31+
* The fix requires that at least one of uri / baseURI starts with "file:"
32+
* AND neither carries a different explicit scheme. This prevents a live-SSRF
33+
* attack where an https: (or http:, ftp:, etc.) uri was incorrectly accepted
34+
* solely because baseURI happened to start with "file:".
35+
*/
36+
class ResolverFilesystemTest {
37+
38+
static {
39+
org.apache.xml.security.Init.init();
40+
}
41+
42+
// -------------------------------------------------------------------------
43+
// canResolve() — scheme bypass tests
44+
// -------------------------------------------------------------------------
45+
46+
/**
47+
* Baseline: a plain file: URI with a file: baseURI is accepted (expected).
48+
*/
49+
@Test
50+
void testFileUriWithFileBaseUriIsAccepted() {
51+
ResolverFilesystem resolver = new ResolverFilesystem();
52+
assertNotNull(resolver.canResolve("file:///etc/hosts", "file:///var/app/"),
53+
"A file: URI with a file: baseURI should be accepted");
54+
}
55+
56+
/**
57+
* FIX: https: uri must be rejected even when baseURI is file:.
58+
*/
59+
@Test
60+
void testHttpsUriIsRejectedWhenBaseUriIsFile() {
61+
ResolverFilesystem resolver = new ResolverFilesystem();
62+
assertNull(resolver.canResolve("https://attacker.com/payload", "file:///var/app/"),
63+
"FIX VERIFIED: https: uri must be rejected even with a file: baseURI");
64+
}
65+
66+
/**
67+
* FIX: http: uri must also be rejected when baseURI is file:.
68+
*/
69+
@Test
70+
void testHttpUriIsRejectedWhenBaseUriIsFile() {
71+
ResolverFilesystem resolver = new ResolverFilesystem();
72+
assertNull(resolver.canResolve("http://attacker.com/payload", "file:///var/app/"),
73+
"FIX VERIFIED: http: uri must be rejected even with a file: baseURI");
74+
}
75+
76+
/**
77+
* FIX: ftp: and other non-file schemes must also be rejected.
78+
*/
79+
@Test
80+
void testFtpUriIsRejectedWhenBaseUriIsFile() {
81+
ResolverFilesystem resolver = new ResolverFilesystem();
82+
assertNull(resolver.canResolve("ftp://attacker.com/file.xml", "file:///var/app/"),
83+
"FIX VERIFIED: ftp: uri must be rejected even with a file: baseURI");
84+
}
85+
86+
/**
87+
* A relative uri (no scheme) combined with a file: baseURI is accepted —
88+
* this is the primary legitimate use case.
89+
*/
90+
@Test
91+
void testRelativeUriWithFileBaseUriIsAccepted() {
92+
ResolverFilesystem resolver = new ResolverFilesystem();
93+
assertNotNull(resolver.canResolve("subdoc.xml", "file:///var/app/"),
94+
"A relative uri with a file: baseURI must be accepted");
95+
}
96+
97+
/**
98+
* VULN: URI.resolve() returns an absolute uri unchanged, so the final URL
99+
* opened by getInputStreamFromExternalReference() will be the attacker's
100+
* https: URL — a live SSRF. Demonstrate that resolve() does not anchor
101+
* the result under the file: base.
102+
*/
103+
@Test
104+
void testUriResolveLeavesAbsoluteUriUnchanged() throws Exception {
105+
java.net.URI base = new java.net.URI("file:///var/app/");
106+
java.net.URI absolute = new java.net.URI("https://attacker.com/payload");
107+
108+
java.net.URI resolved = base.resolve(absolute);
109+
110+
assertFalse("file".equals(resolved.getScheme()),
111+
"VULN CONFIRMED: resolved URI scheme is '" + resolved.getScheme()
112+
+ "', not 'file' — toURL().openStream() will make an outbound HTTPS request");
113+
}
114+
115+
// -------------------------------------------------------------------------
116+
// Sanity checks — cases that should correctly be rejected
117+
// -------------------------------------------------------------------------
118+
119+
/**
120+
* An https: URI with no baseURI is correctly rejected.
121+
*/
122+
@Test
123+
void testHttpsUriWithNullBaseUriIsRejected() {
124+
ResolverFilesystem resolver = new ResolverFilesystem();
125+
assertNull(resolver.canResolve("https://attacker.com/payload", null),
126+
"https: URI with null baseURI should not be accepted");
127+
}
128+
129+
/**
130+
* An https: URI with an https: baseURI is correctly rejected.
131+
*/
132+
@Test
133+
void testHttpsUriWithHttpsBaseUriIsRejected() {
134+
ResolverFilesystem resolver = new ResolverFilesystem();
135+
assertNull(resolver.canResolve("https://attacker.com/payload", "https://victim.com/"),
136+
"https: URI with https: baseURI should not be accepted");
137+
}
138+
139+
/**
140+
* A null URI is correctly rejected.
141+
*/
142+
@Test
143+
void testNullUriIsRejected() {
144+
ResolverFilesystem resolver = new ResolverFilesystem();
145+
assertNull(resolver.canResolve(null, "file:///var/app/"),
146+
"null URI should always be rejected");
147+
}
148+
149+
}

0 commit comments

Comments
 (0)