Route the platform's own SPARQL SERVICE through the egress proxy (imports, PATCH) - #388
Merged
Merged
Conversation
…ot reach the deployment's internal services, and two HTTP tests now say so. Unlike a query to /sparql, these run in the platform's own JVM - PATCH executes the update on the document's graph in memory, an import runs the mapping over each row - so the egress proxy in front of the triplestores never sees the call, and an internal target hands a writer the admin store's agents and authorizations. PATCH-service-internal.sh patches an item with INSERT { <item> <urn:test:leak> ?g } WHERE { SERVICE SILENT <http://fuseki-admin:3030/ds/> { ... } } and fails if the leak triple lands in the document; import-rdf-service-internal.sh imports with rdf-service-internal.rq, a CONSTRUCT that always mints the item and gives it the leak triple only if the admin store answered. SERVICE SILENT turns a refused call into one empty solution, so both check the document for data rather than the response for a status code.
The leak is real on the current code: reproduced against a live stack whose triplestore already sits behind the egress proxy, a PATCH SERVICE to the admin store copied an admin graph name into the patched document. The default stack has no restriction on SERVICE in these paths, so both tests fail against it until the platform routes or refuses these calls itself.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s proxy, closing the leak PATCH-service-internal.sh and import-rdf-service-internal.sh document. A PATCH update runs on the document's graph in memory and an import runs its mapping over each row, both in the platform's JVM, so the proxy in front of the triplestores never saw those SERVICE calls and a writer could name an internal service and copy the admin store's agents and authorizations into a document they read. Application.configureServiceExecution now sets, on the global ARQ context the in-JVM query and update execution reads, either Jena's httpQueryClient - an HttpClient whose ProxySelector sends every SERVICE request through egress, localhost included, so federation with public endpoints keeps working while internal addresses are refused with redirect hops and DNS answers checked - or, when no proxy is configured, httpServiceAllowed=false so SERVICE is disabled rather than left open. It is scoped to SERVICE alone: the platform's own SPARQL calls to its stores use their own HTTP clients, and ALLOW_INTERNAL_URLS leaves SERVICE unrestricted as it already disables the rest of the SSRF protection. EGRESS_PROXY (default egress:3128) reaches the JVM as a system property through CATALINA_OPTS, the pattern the other infrastructure settings already use rather than the parameter-saturated context.xsl plumbing, and the linkeddatahub service depends on egress. Verified: on an in-memory QueryExecution like PATCH and imports build, httpServiceAllowed=false makes SERVICE SILENT yield no triples and a non-silent SERVICE throw QueryDeniedException, and a ProxySelector client sends the request to the proxy that then allows public and denies internal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last variant of the
SERVICESSRF hole: the platform's own in-JVM SPARQL. Two HTTP tests plus the fix.The hole
SERVICEis executed by whoever runs the query. The egress proxy (#387) fronts the triplestores, soSERVICEin a query to/sparqlis covered. But PATCH updates and import mappings run in the platform's own JVM — PATCH on the document's graph in memory, imports over each row — so theirSERVICEcalls don't pass through that proxy. A writer could putSERVICE <http://fuseki-admin:3030/ds/>in a PATCH or an import mapping and copy the admin store's agents and authorizations into a document they can read. Reproduced on a live stack whose triplestores were already behind the egress proxy: a PATCH copied an admin graph name into the patched document.Tests (committed first)
http-tests/document-hierarchy/PATCH-service-internal.sh— PATCH withSERVICE SILENT <fuseki-admin>in itsWHERE; fails if the leak triple lands in the document.http-tests/imports/import-rdf-service-internal.sh+rdf-service-internal.rq— an import whose CONSTRUCT does the same.Both use
SERVICE SILENT, so they check the document for data, not a status code.Fix
Application.configureServiceExecutionsets, on the global ARQ context (which the in-JVM query/update execution reads):httpQueryClient, anHttpClientwhoseProxySelectorsends everySERVICErequest through the egress proxy (EGRESS_PROXY, defaultegress:3128). Public federation keeps working; internal addresses are refused, redirect hops and DNS answers included;ProxySelector.of()coverslocalhosttoo.httpServiceAllowed=false, soSERVICEis disabled rather than left open.ALLOW_INTERNAL_URLSis set, as it already turns off the rest of the SSRF protection.Scoped to
SERVICEalone: the platform's own SPARQL calls to its stores use their own HTTP clients.EGRESS_PROXYreaches the JVM as a system property viaCATALINA_OPTS, the pattern the other infrastructure settings use, andlinkeddatahubnowdepends_on: egress.Federation still works. The shipped stack defaults
EGRESS_PROXY=egress:3128, soSERVICEis proxied, not forbidden — public endpoints answer, only internal ones are refused.Verified
QueryExecution(what PATCH/imports build):httpServiceAllowed=falsemakesSERVICE SILENTyield no triples and a non-silentSERVICEthrowQueryDeniedException.ProxySelectorclient sends the request to the proxy, which allows public and denies internal (matching the earlier linkeddatahub.com run whereSERVICEto Wikidata answered and internal targets wereTCP_DENIED/403).mvn compileclean.The two tests fail on the current default stack (leak reproduced) and pass with this fix routing
SERVICEthrough egress. The full HTTP suite runs in CI.🤖 Generated with Claude Code