Skip to content

Commit 9b0022b

Browse files
committed
Passing full context dataset to XSLT
`$ALLOW_INTERNAL_URLS` env config flag App dropdown shows both user-defined and system dataspaces (apps)
1 parent c55338e commit 9b0022b

15 files changed

Lines changed: 171 additions & 139 deletions

File tree

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ services:
6363
- SELF_SIGNED_CERT=true # only on localhost
6464
- SIGN_UP_CERT_VALIDITY=180
6565
- MAX_CONTENT_LENGTH=${MAX_CONTENT_LENGTH:-2097152}
66+
- ALLOW_INTERNAL_URLS=${ALLOW_INTERNAL_URLS:-}
6667
- NOTIFICATION_ADDRESS=LinkedDataHub <notifications@localhost>
6768
- MAIL_SMTP_HOST=email-server
6869
- MAIL_SMTP_PORT=25

platform/entrypoint.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,10 @@ if [ -n "$ENABLE_LINKED_DATA_PROXY" ]; then
10341034
ENABLE_LINKED_DATA_PROXY_PARAM="--stringparam ldhc:enableLinkedDataProxy '$ENABLE_LINKED_DATA_PROXY' "
10351035
fi
10361036

1037+
if [ -n "$ALLOW_INTERNAL_URLS" ]; then
1038+
export CATALINA_OPTS="$CATALINA_OPTS -Dcom.atomgraph.linkeddatahub.allowInternalUrls=$ALLOW_INTERNAL_URLS"
1039+
fi
1040+
10371041
if [ -n "$MAX_CONTENT_LENGTH" ]; then
10381042
MAX_CONTENT_LENGTH_PARAM="--stringparam ldhc:maxContentLength '$MAX_CONTENT_LENGTH' "
10391043
fi

src/main/java/com/atomgraph/linkeddatahub/Application.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
import com.atomgraph.linkeddatahub.server.security.AgentContext;
114114
import com.atomgraph.linkeddatahub.server.security.AuthorizationContext;
115115
import com.atomgraph.linkeddatahub.server.util.MessageBuilder;
116+
import com.atomgraph.linkeddatahub.server.util.URLValidator;
116117
import com.atomgraph.linkeddatahub.vocabulary.ACL;
117118
import com.atomgraph.linkeddatahub.vocabulary.FOAF;
118119
import com.atomgraph.linkeddatahub.vocabulary.LDH;
@@ -284,6 +285,8 @@ public class Application extends ResourceConfig
284285
private final boolean invalidateCache;
285286
private final Integer cookieMaxAge;
286287
private final boolean enableLinkedDataProxy;
288+
private final boolean allowInternalUrls;
289+
private final URLValidator urlValidator;
287290
private final Integer maxContentLength;
288291
private final Address notificationAddress;
289292
private final Authenticator authenticator;
@@ -347,6 +350,8 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti
347350
servletConfig.getServletContext().getInitParameter(LDHC.invalidateCache.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(LDHC.invalidateCache.getURI())) : false,
348351
servletConfig.getServletContext().getInitParameter(LDHC.cookieMaxAge.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(LDHC.cookieMaxAge.getURI())) : null,
349352
servletConfig.getServletContext().getInitParameter(LDHC.enableLinkedDataProxy.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(LDHC.enableLinkedDataProxy.getURI())) : true,
353+
System.getProperty("com.atomgraph.linkeddatahub.allowInternalUrls") != null ? Boolean.parseBoolean(System.getProperty("com.atomgraph.linkeddatahub.allowInternalUrls")) :
354+
servletConfig.getServletContext().getInitParameter(LDHC.allowInternalUrls.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(LDHC.allowInternalUrls.getURI())) : false,
350355
servletConfig.getServletContext().getInitParameter(LDHC.maxContentLength.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(LDHC.maxContentLength.getURI())) : null,
351356
servletConfig.getServletContext().getInitParameter(LDHC.maxConnPerRoute.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(LDHC.maxConnPerRoute.getURI())) : null,
352357
servletConfig.getServletContext().getInitParameter(LDHC.maxTotalConn.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(LDHC.maxTotalConn.getURI())) : null,
@@ -404,6 +409,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti
404409
* @param invalidateCache true if Varnish proxy cache should be invalidated
405410
* @param cookieMaxAge max age of auth cookies
406411
* @param enableLinkedDataProxy true if Linked Data proxy is enabled
412+
* @param allowInternalUrls true if internal/private network URLs are allowed (disables SSRF protection)
407413
* @param maxContentLength maximum size of request entity
408414
* @param maxConnPerRoute maximum client connections per rout
409415
* @param maxTotalConn maximum total client connections
@@ -436,7 +442,7 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType
436442
final String webIDQueryString, final String agentQueryString, final String userAccountQueryString, final String ontologyQueryString,
437443
final String baseURIString, final String proxyScheme, final String proxyHostname, final Integer proxyPort,
438444
final String uploadRootString, final boolean invalidateCache,
439-
final Integer cookieMaxAge, final boolean enableLinkedDataProxy, final Integer maxContentLength,
445+
final Integer cookieMaxAge, final boolean enableLinkedDataProxy, final boolean allowInternalUrls, final Integer maxContentLength,
440446
final Integer maxConnPerRoute, final Integer maxTotalConn, final Integer maxRequestRetries, final Integer maxImportThreads,
441447
final String notificationAddressString, final String supportedLanguageCodes, final boolean enableWebIDSignUp, final String oidcRefreshTokensPropertiesPath,
442448
final String frontendProxyString, final String backendProxyAdminString, final String backendProxyEndUserString,
@@ -570,6 +576,8 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType
570576
this.cacheStylesheet = cacheStylesheet;
571577
this.resolvingUncached = resolvingUncached;
572578
this.enableLinkedDataProxy = enableLinkedDataProxy;
579+
this.allowInternalUrls = allowInternalUrls;
580+
this.urlValidator = new URLValidator(allowInternalUrls);
573581
this.maxContentLength = maxContentLength;
574582
this.invalidateCache = invalidateCache;
575583
this.enableWebIDSignUp = enableWebIDSignUp;
@@ -2159,7 +2167,27 @@ public boolean isEnableLinkedDataProxy()
21592167
{
21602168
return enableLinkedDataProxy;
21612169
}
2162-
2170+
2171+
/**
2172+
* Returns true if internal/private network URLs are allowed (SSRF protection disabled).
2173+
*
2174+
* @return true if internal URLs are allowed
2175+
*/
2176+
public boolean isAllowInternalUrls()
2177+
{
2178+
return allowInternalUrls;
2179+
}
2180+
2181+
/**
2182+
* Returns the shared URLValidator instance configured for this application.
2183+
*
2184+
* @return the URL validator
2185+
*/
2186+
public URLValidator getURLValidator()
2187+
{
2188+
return urlValidator;
2189+
}
2190+
21632191
/**
21642192
* Maximum allowed request body size.
21652193
*

src/main/java/com/atomgraph/linkeddatahub/resource/Transform.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.atomgraph.linkeddatahub.server.io.ValidatingModelProvider;
2424
import com.atomgraph.linkeddatahub.server.model.impl.DirectGraphStoreImpl;
2525
import com.atomgraph.linkeddatahub.server.security.AgentContext;
26-
import com.atomgraph.linkeddatahub.server.util.URLValidator;
2726
import com.atomgraph.linkeddatahub.vocabulary.NFO;
2827
import com.atomgraph.spinrdf.vocabulary.SPIN;
2928
import java.net.URI;
@@ -143,8 +142,8 @@ public Response post(Model model)
143142
if (queryRes == null) throw new BadRequestException("Transformation query string (spin:query) not provided");
144143

145144
// LNK-002: Validate URIs to prevent SSRF attacks
146-
new URLValidator(URI.create(queryRes.getURI())).validate();
147-
new URLValidator(URI.create(source.getURI())).validate();
145+
getSystem().getURLValidator().validate(URI.create(queryRes.getURI()));
146+
getSystem().getURLValidator().validate(URI.create(source.getURI()));
148147

149148
GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes()).
150149
delegation(getUriInfo().getBaseUri(), getAgentContext().orElse(null));
@@ -235,7 +234,7 @@ public Response postFileBodyPart(Model model, Map<String, FormDataBodyPart> file
235234
if (queryRes == null) throw new BadRequestException("Transformation query string (spin:query) not provided");
236235

237236
// LNK-002: Validate query URI to prevent SSRF attacks
238-
new URLValidator(URI.create(queryRes.getURI())).validate();
237+
getSystem().getURLValidator().validate(URI.create(queryRes.getURI()));
239238

240239
GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes()).
241240
delegation(getUriInfo().getBaseUri(), getAgentContext().orElse(null));

src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.atomgraph.linkeddatahub.client.GraphStoreClient;
2424
import com.atomgraph.linkeddatahub.resource.admin.ClearOntology;
2525
import com.atomgraph.linkeddatahub.server.security.AgentContext;
26-
import com.atomgraph.linkeddatahub.server.util.URLValidator;
2726
import com.atomgraph.linkeddatahub.server.util.XSLTMasterUpdater;
2827
import static com.atomgraph.server.status.UnprocessableEntityStatus.UNPROCESSABLE_ENTITY;
2928
import jakarta.inject.Inject;
@@ -134,7 +133,7 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("
134133
}
135134

136135
// Validate package URI to prevent SSRF attacks
137-
new URLValidator(URI.create(packageURI)).validate();
136+
getSystem().getURLValidator().validate(URI.create(packageURI));
138137

139138
if (log.isInfoEnabled()) log.info("Installing package: {}", packageURI);
140139
com.atomgraph.linkeddatahub.apps.model.Package pkg = getPackage(packageURI);
@@ -161,7 +160,7 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("
161160
if (ontology != null)
162161
{
163162
// Validate ontology URI to prevent SSRF attacks
164-
new URLValidator(URI.create(ontology.getURI())).validate();
163+
getSystem().getURLValidator().validate(URI.create(ontology.getURI()));
165164

166165
if (log.isDebugEnabled()) log.debug("Downloading package ontology from: {}", ontology.getURI());
167166
Model ontologyModel = downloadOntology(ontology.getURI());
@@ -175,7 +174,7 @@ public Response post(@FormParam("package-uri") String packageURI, @HeaderParam("
175174
String packagePath = pkg.getStylesheetPath();
176175

177176
// Validate stylesheet URI to prevent SSRF attacks
178-
new URLValidator(stylesheetURI).validate();
177+
getSystem().getURLValidator().validate(stylesheetURI);
179178

180179
if (log.isDebugEnabled()) log.debug("Downloading package stylesheet from: {}", stylesheetURI);
181180
String stylesheetContent = downloadStylesheet(stylesheetURI);

src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import com.atomgraph.linkeddatahub.server.exception.auth.webid.WebIDLoadingException;
2626
import com.atomgraph.linkeddatahub.server.exception.auth.webid.WebIDDelegationException;
2727
import com.atomgraph.linkeddatahub.server.security.WebIDSecurityContext;
28-
import com.atomgraph.linkeddatahub.server.util.URLValidator;
2928
import com.atomgraph.linkeddatahub.vocabulary.ACL;
3029
import com.atomgraph.linkeddatahub.vocabulary.Cert;
3130
import com.atomgraph.linkeddatahub.vocabulary.FOAF;
@@ -129,7 +128,7 @@ public SecurityContext authenticate(ContainerRequestContext request)
129128
}
130129
if (log.isTraceEnabled()) log.trace("Client WebID: {}", webID);
131130

132-
new URLValidator(webID).validate(); // LNK-004: Prevent SSRF via WebID URI
131+
getSystem().getURLValidator().validate(webID); // LNK-004: Prevent SSRF via WebID URI
133132
Resource agent = authenticate(loadWebID(webID), webID, publicKey);
134133
if (agent == null)
135134
{
@@ -142,7 +141,7 @@ public SecurityContext authenticate(ContainerRequestContext request)
142141
if (onBehalfOf != null)
143142
{
144143
URI principalWebID = new URI(onBehalfOf);
145-
new URLValidator(principalWebID).validate(); // LNK-004: Prevent SSRF via On-Behalf-Of header
144+
getSystem().getURLValidator().validate(principalWebID); // LNK-004: Prevent SSRF via On-Behalf-Of header
146145
Model principalWebIDModel = loadWebID(principalWebID);
147146
Resource principal = principalWebIDModel.createResource(onBehalfOf);
148147
// if we verify that the current agent is a secretary of the principal, that principal becomes current agent. Else throw error
@@ -300,7 +299,7 @@ public Model loadWebIDFromURI(URI webID)
300299
if (certKeyRes != null && certKeyRes.isURIResource())
301300
{
302301
URI certKey = URI.create(certKeyRes.getURI());
303-
new URLValidator(certKey).validate(); // LNK-004: Prevent SSRF via cert:key reference in WebID document
302+
getSystem().getURLValidator().validate(certKey); // LNK-004: Prevent SSRF via cert:key reference in WebID document
304303
// remove fragment identifier to get document URI
305304
URI certKeyDoc = new URI(certKey.getScheme(), certKey.getSchemeSpecificPart(), null).normalize();
306305

src/main/java/com/atomgraph/linkeddatahub/server/model/impl/ProxiedGraph.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import com.atomgraph.linkeddatahub.server.security.AgentContext;
3232
import com.atomgraph.linkeddatahub.server.security.IDTokenSecurityContext;
3333
import com.atomgraph.linkeddatahub.server.security.WebIDSecurityContext;
34-
import com.atomgraph.linkeddatahub.server.util.URLValidator;
3534
import java.net.URI;
3635
import java.util.ArrayList;
3736
import java.util.List;
@@ -309,7 +308,7 @@ public Response get(WebTarget target, Invocation.Builder builder)
309308

310309
if (!getSystem().isEnableLinkedDataProxy()) throw new NotAllowedException("Linked Data proxy not enabled");
311310
// LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
312-
new URLValidator(target.getUri()).validate();
311+
getSystem().getURLValidator().validate(target.getUri());
313312

314313
return super.get(target, builder);
315314
}
@@ -326,7 +325,7 @@ public Response post(String sparqlQuery)
326325
{
327326
if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied");
328327
// LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
329-
new URLValidator(getWebTarget().getUri()).validate();
328+
getSystem().getURLValidator().validate(getWebTarget().getUri());
330329

331330
if (log.isDebugEnabled()) log.debug("POSTing SPARQL query to URI: {}", getWebTarget().getUri());
332331

@@ -360,7 +359,7 @@ public Response postForm(String formData)
360359
{
361360
if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied");
362361
// LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
363-
new URLValidator(getWebTarget().getUri()).validate();
362+
getSystem().getURLValidator().validate(getWebTarget().getUri());
364363

365364
if (log.isDebugEnabled()) log.debug("POSTing form data to URI: {}", getWebTarget().getUri());
366365

@@ -394,7 +393,7 @@ public Response patch(String sparqlUpdate)
394393
{
395394
if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied");
396395
// LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
397-
new URLValidator(getWebTarget().getUri()).validate();
396+
getSystem().getURLValidator().validate(getWebTarget().getUri());
398397

399398
if (log.isDebugEnabled()) log.debug("PATCHing SPARQL update to URI: {}", getWebTarget().getUri());
400399

@@ -429,7 +428,7 @@ public Response postMultipart(FormDataMultiPart multiPart)
429428
if (!getSystem().isEnableLinkedDataProxy()) throw new NotAllowedException("Linked Data proxy not enabled");
430429
if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied"); // cannot throw Exception in constructor: https://github.com/eclipse-ee4j/jersey/issues/4436
431430
// LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
432-
new URLValidator(getWebTarget().getUri()).validate();
431+
getSystem().getURLValidator().validate(getWebTarget().getUri());
433432

434433
try (Response cr = getWebTarget().request().
435434
accept(getMediaTypes().getReadable(Model.class).toArray(jakarta.ws.rs.core.MediaType[]::new)).
@@ -453,7 +452,7 @@ public Response putMultipart(FormDataMultiPart multiPart)
453452
if (!getSystem().isEnableLinkedDataProxy()) throw new NotAllowedException("Linked Data proxy not enabled");
454453
if (getWebTarget() == null) throw new NotFoundException("Resource URI not supplied"); // cannot throw Exception in constructor: https://github.com/eclipse-ee4j/jersey/issues/4436
455454
// LNK-009: Validate that proxied URI is not internal/private (SSRF protection)
456-
new URLValidator(getWebTarget().getUri()).validate();
455+
getSystem().getURLValidator().validate(getWebTarget().getUri());
457456

458457
try (Response cr = getWebTarget().request().
459458
accept(getMediaTypes().getReadable(Model.class).toArray(jakarta.ws.rs.core.MediaType[]::new)).

src/main/java/com/atomgraph/linkeddatahub/server/util/URLValidator.java

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,16 @@ public class URLValidator
3737
{
3838
private static final Logger log = LoggerFactory.getLogger(URLValidator.class);
3939

40-
private final URI uri;
40+
private final boolean allowInternal;
4141

4242
/**
43-
* Constructs URL validator for the given URI.
43+
* Constructs URL validator.
4444
*
45-
* @param uri the URI to validate
46-
* @throws IllegalArgumentException if the URI is null
45+
* @param allowInternal if true, internal/private network addresses are allowed (disables SSRF protection)
4746
*/
48-
public URLValidator(URI uri)
47+
public URLValidator(boolean allowInternal)
4948
{
50-
if (uri == null) throw new IllegalArgumentException("URI cannot be null");
51-
this.uri = uri;
49+
this.allowInternal = allowInternal;
5250
}
5351

5452
/**
@@ -61,44 +59,40 @@ public URLValidator(URI uri)
6159
* may legitimately need to access resources on the same server (e.g., transformation queries,
6260
* WebID documents during development, admin operations).
6361
*
62+
* @param uri the URI to validate
6463
* @return the validated URI
65-
* @throws IllegalArgumentException if the URI host is null
64+
* @throws IllegalArgumentException if the URI is null or its host is null
6665
* @throws InternalURLException if the URI resolves to an internal IP address
6766
*/
68-
public URI validate()
67+
public URI validate(URI uri)
6968
{
70-
String host = uri.getHost();
71-
if (host == null) throw new IllegalArgumentException("URI host cannot be null");
69+
if (uri == null) throw new IllegalArgumentException("URI cannot be null");
7270

73-
// Resolve hostname to IP and check if it's private/internal
74-
try
71+
if (!allowInternal)
7572
{
76-
InetAddress address = InetAddress.getByName(host);
73+
String host = uri.getHost();
74+
if (host == null) throw new IllegalArgumentException("URI host cannot be null");
7775

78-
// Note: We don't block loopback addresses (127.0.0.1, localhost) because the application
79-
// legitimately accesses its own endpoints for various operations
76+
// Resolve hostname to IP and check if it's private/internal
77+
try
78+
{
79+
InetAddress address = InetAddress.getByName(host);
8080

81-
if (address.isLinkLocalAddress())
82-
throw new InternalURLException(uri, address.getHostAddress());
83-
if (address.isSiteLocalAddress())
84-
throw new InternalURLException(uri, address.getHostAddress());
85-
}
86-
catch (UnknownHostException e)
87-
{
88-
if (log.isWarnEnabled()) log.warn("Could not resolve hostname for SSRF validation: {}", host);
89-
// Allow request to proceed - will fail later with better error message
90-
}
81+
// Note: We don't block loopback addresses (127.0.0.1, localhost) because the application
82+
// legitimately accesses its own endpoints for various operations
9183

92-
return uri;
93-
}
84+
if (address.isLinkLocalAddress())
85+
throw new InternalURLException(uri, address.getHostAddress());
86+
if (address.isSiteLocalAddress())
87+
throw new InternalURLException(uri, address.getHostAddress());
88+
}
89+
catch (UnknownHostException e)
90+
{
91+
if (log.isWarnEnabled()) log.warn("Could not resolve hostname for SSRF validation: {}", host);
92+
// Allow request to proceed - will fail later with better error message
93+
}
94+
}
9495

95-
/**
96-
* Returns the URI being validated.
97-
*
98-
* @return the URI
99-
*/
100-
public URI getURI()
101-
{
10296
return uri;
10397
}
10498

0 commit comments

Comments
 (0)