COAR DOI checks - #12538
Conversation
There was a problem hiding this comment.
Claude
PR #12538 Review: COAR DOI checks
What it does
The COAR Notify Relationship Announcement inbox handler (/api/inbox) fetches an attacker-supplied subjectId URL, follows its redirect, reads Signposting Link headers from the redirect target, and — if one of those links claims to be DataCite XML — fetches that URL too and parses it as XML. Before this PR, that third-step URL was fetched unconditionally, meaning a malicious LDN sender could point Dataverse at an arbitrary internal/external URL via a crafted Link header (classic SSRF). This PR adds isTrustedDataCiteUrl(), which only allows the fetch to proceed if the URL is a well-formed DOI under a fixed set of trusted hosts (doi.org, dx.doi.org, api.datacite.org, api.test.datacite.org, both http/https). It also adds a unit test and a docs clarification recommending against allowed-hosts=* in production.
Correctness
- The prefix constants (AbstractDOIProvider.DOI_RESOLVER_URL etc.) all include a trailing /, so startsWith() can't be bypassed with tricks like https://doi.org.evil.com/....
- The remainder of the URL is validated by UnmanagedDOIProvider.parsePersistentId(), which enforces real DOI syntax (10. authority prefix, via PidProvider.checkDOIAuthority) — so it's not just a host check, it's host + well-formed-DOI, which is a solid pairing.
- parseDataCiteXml() fetches the exact string that was validated (no rebuild/re-decode step in between), so there's no TOCTOU or re-parsing gap between the check and the fetch.
- I verified the constructor signature used in the new test (DatasetServiceBean, UserNotificationServiceBean, DataverseRoleServiceBean, RoleAssigneeServiceBean) matches COARNotifyRelationshipAnnouncement's actual constructor — compiles fine.
Residual scope (not a blocker, worth noting)
- Case-sensitive startsWith means a redirect target using HTTPS://DOI.ORG/... would be (harmlessly) rejected — a minor robustness nitpick, not a security issue.
Style / minor
- isTrustedDataCiteUrl uses a chain of literal string prefixes for the DataCite API hosts ("https://api.datacite.org/dois/", etc.) instead of named constants like the DOI-resolver ones — mild inconsistency, could pull these into constants in AbstractDOIProvider alongside DOI_RESOLVER_URL for symmetry, but not a real problem.
- The pre-existing duplicated Javadoc comment above extractDataCiteXmlUrl (lines ~193/197 in the full file) isn't touched by this PR — flagging only so it isn't mistaken for something this PR introduced.
Test coverage
- The new COARNotifyRelationshipAnnouncementTest covers the meaningful cases well: valid DOI via each trusted host/scheme, syntactically invalid DOI on a trusted host, untrusted host (including a nice adversarial case — https://malicious.org/doi.org/10.1234/5678, checking the prefix isn't just a substring match), and null/empty input.
- No test exercises retrieveResourceMetadata() end-to-end to confirm parseDataCiteXml is actually skipped when isTrustedDataCiteUrl returns false (i.e., an integration-style check that the warning path is taken and no HTTP call is attempted) — the current test only unit-tests the predicate in isolation. Given the predicate is thoroughly tested and the call site is a simple one-line if, this is a minor gap, not a real risk.
Verdict
Solid, well-tested, appropriately narrow security fix. No blocking issues.
|
Dropped support for api URLs originally added as we don't expect anyone to be referencing a DOI using those URLs. |
What this PR does / why we need it: The COAR Notify Relationship Announcement processor can call DataCite and use Datacite metadata to find the title and type of a related resource. This PR avoids unnecessary calls if the PID is not a DOI.
Which issue(s) this PR closes:
Special notes for your reviewer: Also tweaked the docs to repeat the recommendation to not use allowed-hosts=* in production in the config guide (it was already noted in https://guides.dataverse.org/en/latest/api/linkeddatanotification.html#linked-data-notification-api ).
Suggestions on how to test this: Unit test included. Nominally could test with a DSpace instance or manually generated messages as well if you can detect the outgoing call being suppressed.
Does this PR introduce a user interface change? If mockups are available, please link/include them here:
Is there a release notes update needed for this change?:
Additional documentation: