Skip to content

[SECURITY] Prevent SSRF and LFI in document loading and validation - #2222

Open
stweil wants to merge 3 commits into
kitodo:mainfrom
stweil:fix/dom-document-validation-ssrf
Open

stweil wants to merge 3 commits into
kitodo:mainfrom
stweil:fix/dom-document-validation-ssrf

Conversation

@stweil

@stweil stweil commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

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.

Fix the request handling so that no request is issued unless it is explicitly safe:

  • extract the host validation that the DOMDocumentValidation middleware introduced into a shared Kitodo\Dlf\Service\UrlValidationService, and apply it to every server-side fetch
  • 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
  • additionally resolve AAAA records when checking a host, so that a host with a public A record but a private AAAA record can no longer bypass the check

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, 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, *.wildcards supported). 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. The DOMDocumentValidation middleware keeps its per-type hostAllowList for 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

@sebastian-meyer sebastian-meyer added the ↯ security A critical security related problem. label Oct 21, 2026
@codecov

codecov Bot commented Oct 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (54b67b4) to head (b94e737).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@     Coverage Diff      @@
##   main   #2222   +/-   ##
============================
============================

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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
stweil force-pushed the fix/dom-document-validation-ssrf branch from b94e737 to b9416c2 Compare October 21, 2026 10:26
@stweil stweil changed the title [SECURITY] Prevent SSRF and LFI in DOMDocumentValidation middleware [SECURITY] Prevent SSRF and LFI in document loading and validation Oct 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

↯ security A critical security related problem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants