Skip to content

Commit 9677de3

Browse files
committed
feat: check Host in fabricate
1 parent c3d15f3 commit 9677de3

File tree

2 files changed

+25
-2
lines changed

2 files changed

+25
-2
lines changed

andrvotr-impl/src/main/java/io/github/fmfi_svt/andrvotr/HttpController.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import jakarta.servlet.http.HttpServletResponse;
66
import java.io.IOException;
77
import java.io.OutputStream;
8+
import java.net.URL;
89
import java.nio.charset.StandardCharsets;
910
import java.time.Duration;
1011
import java.time.Instant;
@@ -40,6 +41,8 @@ public final class HttpController extends AbstractInitializableComponent {
4041

4142
private DataSealer dataSealer;
4243

44+
private String idpEntityID;
45+
4346
public void setHttpClient(@Nonnull HttpClient client) {
4447
checkSetterPreconditions();
4548
httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
@@ -55,6 +58,12 @@ public void setDataSealer(@Nonnull DataSealer sealer) {
5558
dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
5659
}
5760

61+
public void setIdpEntityID(@Nonnull String id) {
62+
checkSetterPreconditions();
63+
Constraint.isFalse(Strings.isNullOrEmpty(id), "idpEntityId cannot be null or empty");
64+
idpEntityID = id;
65+
}
66+
5867
@Override
5968
protected void doInitialize() throws ComponentInitializationException {
6069
super.doInitialize();
@@ -68,6 +77,9 @@ protected void doInitialize() throws ComponentInitializationException {
6877
if (null == dataSealer) {
6978
throw new ComponentInitializationException("DataSealer cannot be null");
7079
}
80+
if (Strings.isNullOrEmpty(idpEntityID)) {
81+
throw new ComponentInitializationException("idpEntityId cannot be null or empty");
82+
}
7183
}
7284

7385
@PostMapping("/fabricate")
@@ -97,6 +109,16 @@ public void fabricate(@Nonnull HttpServletRequest httpRequest, @Nonnull HttpServ
97109
return;
98110
}
99111

112+
// Check that the request Host header has the expected value. We will send it a nested request later, and this
113+
// is a little extra protection against SSRF. Sadly, the IdP does not really know its own hostname. We will use
114+
// the host portion of our entityID. This is a hacky approximation. In theory an IdP could use different
115+
// hostnames in its entityID and its SAML endpoints.
116+
String expectedHost = new URL(idpEntityID).getHost();
117+
if (!expectedHost.equals(httpRequest.getServerName())) {
118+
sendError(httpResponse, 400, "Unexpected Host, should be " + expectedHost);
119+
return;
120+
}
121+
100122
if (authorityToken.startsWith("E:")) {
101123
sendError(httpResponse, 403, "Authority token generator error: " + authorityToken);
102124
return;
@@ -130,7 +152,7 @@ public void fabricate(@Nonnull HttpServletRequest httpRequest, @Nonnull HttpServ
130152

131153
String cookies = parts[2];
132154

133-
String expectedPrefix = "https://" + httpRequest.getServerName() + "/idp/profile/SAML2/Redirect/SSO?";
155+
String expectedPrefix = "https://" + expectedHost + "/idp/profile/SAML2/Redirect/SSO?";
134156
if (!targetUrl.startsWith(expectedPrefix)) {
135157
sendError(httpResponse, 403, "Invalid target URL");
136158
return;

andrvotr-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
<bean class="io.github.fmfi_svt.andrvotr.HttpController"
2626
p:httpClient-ref="%{andrvotr.httpclient.bean:andrvotr.DefaultHttpClient}"
2727
p:config-ref="andrvotr.Config"
28-
p:dataSealer-ref="shibboleth.DataSealer" />
28+
p:dataSealer-ref="shibboleth.DataSealer"
29+
p:idpEntityID="%{idp.entityID}" />
2930

3031
<bean id="andrvotr.DefaultHttpClient" parent="shibboleth.HttpClientFactory"
3132
p:disableCookieManagement="true"

0 commit comments

Comments
 (0)