Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2222 +/- ##
============================
============================
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The middleware fetches the user-supplied `url` query parameter server-side and the request is not authenticated, which allows unauthenticated server-side request forgery (SSRF): an attacker could make the application issue requests to arbitrary hosts (internal services, cloud metadata endpoints) and observe the result through the validation response. On top of that, GeneralUtility::getUrl() reads file:// URLs via file_get_contents(), so the middleware also allowed local file inclusion (an existence oracle for any file the PHP process can read, and content echo for XML files). Fix the request handling so that no request is issued unless it is explicitly safe: - read the validation configuration before fetching the URL, so that requests to unconfigured or unknown validation types are rejected without any network side effect - only accept http/https URLs (Helper::isValidHttpUrl) instead of GeneralUtility::isValidUrl, which also allows file://, ftp://, etc. - reject hosts that are not allowed: by default, hosts must resolve to public addresses only; private and reserved ranges (loopback, RFC 1918, link-local incl. the cloud metadata address, CGNAT, multicast, etc.) and unresolvable hosts are rejected. Ranges are checked against an explicit CIDR list instead of FILTER_FLAG_NO_PRIV_RANGE|NO_RES_RANGE, whose coverage varies between PHP versions; IPv4-mapped IPv6 addresses are unwrapped to prevent bypassing the IPv4 checks. - optionally, a validation configuration may define a hostAllowList (comma-separated hostnames / *.wildcards); when set, only these hosts may be fetched, which also allows explicitly permitting an internal service Document the new behaviour and add unit tests covering the SSRF protection (scheme gate, private/reserved ranges, IPv4-mapped IPv6 bypass, hostAllowList matching, wildcard matching). Note: DNS rebinding (a hostname resolving publicly at check time and privately at fetch time) cannot be fully prevented without pinning the resolved address; the fail-closed check of all resolved addresses minimizes the exposure window. Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud) Signed-off-by: Stefan Weil <sw@weilnetz.de>
- inet_ntop() may return false, which must not be passed to isPublicAddress() (and must fail the check) - $binaryIp of isInRange() is guaranteed to be a string, remove the dead false check Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud) Signed-off-by: Stefan Weil <sw@weilnetz.de>
The extension fetched user-supplied URLs server-side without any restriction on the host, and followed redirects without validating the redirect target. This allowed unauthenticated server-side request forgery (SSRF) and local file inclusion (LFI): - "tx_dlf[id]" and "tx_dlf[multiViewSource][]" accept an arbitrary URL that the server then fetches via Helper::getUrl(). That helper validated the scheme only, so any host was reachable, including cloud metadata endpoints (169.254.169.254) and internal services. - IiifUrlReader and FullTextReader used GeneralUtility::getUrl(), which falls back to file_get_contents() for anything that is not http(s). A document could therefore make the server read local files via "file://" URLs (e.g. through a METS FLocat or a JSON-LD "@context"). - The DOMDocumentValidation middleware validated the host of the initial URL, but Guzzle followed up to 5 redirects without re-validating the target, so a redirect to an internal service bypassed the check. Extract the host validation that the DOMDocumentValidation middleware introduced in cfbabe9 into a shared service and apply it to every server-side fetch: - add Kitodo\Dlf\Service\UrlValidationService with the host allow-list and the public-address checks. In addition to the existing IPv4 checks it now resolves AAAA records as well, so that a host with a public A record but a private AAAA record can no longer bypass the check. - make Helper::getUrl() the single chokepoint for server-side fetches: it accepts http(s) only, requires the host to resolve to a public address unless it is allow-listed, and follows redirects manually so that every redirect target is validated again. A redirect target must either stay on the same host or itself be allowed. - replace GeneralUtility::getUrl() in IiifUrlReader and FullTextReader with Helper::getUrl(), which removes the file_get_contents() fallback and adds the missing request timeout. - distinguish trusted from untrusted locations: database records and CLI arguments are fetched without a host check, so that documents hosted on an internal server keep working, while locations that come from request parameters are validated. Operators whose documents or document resources are hosted on an internal host have to list that host in the new extension configuration setting "general.urlAllowList" (comma-separated hosts, "*.wildcards" supported). The DOMDocumentValidation middleware keeps its per-type "hostAllowList" for the same purpose. Add unit tests for the new service and adapt the functional test setup, whose fixtures are served by the local test server. Note: DNS rebinding (a hostname resolving publicly at check time and privately at fetch time) cannot be fully prevented without pinning the resolved address; the fail-closed check of all resolved addresses minimizes the exposure window. Assisted-by: OpenCode / MiniMax M3.1 Flash (MiniMax) Signed-off-by: Stefan Weil <sw@weilnetz.de>
stweil
force-pushed
the
fix/dom-document-validation-ssrf
branch
from
October 21, 2026 10:26
b94e737 to
b9416c2
Compare
This branch has not been deployed
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.
The extension fetched user-supplied URLs server-side without any restriction on the host, and followed redirects without validating the redirect target. This allowed unauthenticated server-side request forgery (SSRF) and local file inclusion (LFI):
tx_dlf[id]andtx_dlf[multiViewSource][]accept an arbitrary URL that the server then fetches viaHelper::getUrl(). That helper validated the scheme only, so any host was reachable, including cloud metadata endpoints (169.254.169.254) and internal services.IiifUrlReaderandFullTextReaderusedGeneralUtility::getUrl(), which falls back tofile_get_contents()for anything that is not http(s). A document could therefore make the server read local files viafile://URLs (e.g. through a METSFLocator a JSON-LD@context).DOMDocumentValidationmiddleware validated the host of the initial URL, but Guzzle followed up to 5 redirects without re-validating the target, so a redirect to an internal service bypassed the check.Fix the request handling so that no request is issued unless it is explicitly safe:
Kitodo\Dlf\Service\UrlValidationService, and apply it to every server-side fetchHelper::getUrl()the single chokepoint for server-side fetches: it accepts http(s) only, requires the host to resolve to a public address unless it is allow-listed, and follows redirects manually so that every redirect target is validated again. A redirect target must either stay on the same host or itself be allowedGeneralUtility::getUrl()inIiifUrlReaderandFullTextReaderwithHelper::getUrl(), which removes thefile_get_contents()fallback and adds the missing request timeoutDocument the new behaviour and add unit tests covering the SSRF protection (scheme gate, private/reserved ranges, IPv4-mapped IPv6 bypass, hostAllowList matching, wildcard matching, relative redirect resolution).
Operator impact: hosts that are not publicly routable have to be listed in the new extension configuration setting
general.urlAllowList(comma-separated host names,*.wildcardssupported). This is required for documents or document resources (e.g. an IIIF image server or an annotation server) that are hosted on an internal host; otherwise those documents are no longer loaded. TheDOMDocumentValidationmiddleware keeps its per-typehostAllowListfor the same purpose.Note: DNS rebinding (a hostname resolving publicly at check time and privately at fetch time) cannot be fully prevented without pinning the resolved address; the fail-closed check of all resolved addresses minimizes the exposure window.
Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
Assisted-by: OpenCode / MiniMax M3.1 Flash (MiniMax)
Signed-off-by: Stefan Weil sw@weilnetz.de