From 8b48693c0aa887bb98d798266b8fb3992d756fa9 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 15 Jul 2026 13:55:30 -0400 Subject: [PATCH 1/4] validate any DOI URL sent --- .../COARNotifyRelationshipAnnouncement.java | 42 ++++++++++++++- ...OARNotifyRelationshipAnnouncementTest.java | 52 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java index 84f5702cf40..fefeb23edf8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java @@ -9,6 +9,8 @@ import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.pidproviders.PidProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.pidproviders.doi.UnmanagedDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.json.JsonLDNamespace; import edu.harvard.iq.dataverse.util.json.JsonLDTerm; @@ -168,7 +170,11 @@ private ResourceMetadata retrieveResourceMetadata(String subjectId) { // Step 3: Retrieve and parse DataCite XML if (dataciteXmlUrl != null) { - parseDataCiteXml(dataciteXmlUrl, client, metadata); + if (isTrustedDataCiteUrl(dataciteXmlUrl)) { + parseDataCiteXml(dataciteXmlUrl, client, metadata); + } else { + logger.warning("DataCite XML URL is not from a trusted source: " + dataciteXmlUrl); + } } else { logger.fine("No DataCite XML URL found in Signposting links"); } @@ -232,6 +238,40 @@ private String extractDataCiteXmlUrl(CloseableHttpResponse headResponse) { return null; } + /** + * Validate that the URL is a trusted source for DataCite XML. + * Supports standard DOI resolvers and DataCite API. + */ + boolean isTrustedDataCiteUrl(String url) { + if (url == null || url.isBlank()) { + return false; + } + + String doiPart = null; + if (url.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL)) { + doiPart = url.substring(AbstractDOIProvider.DOI_RESOLVER_URL.length()); + } else if (url.startsWith(AbstractDOIProvider.HTTP_DOI_RESOLVER_URL)) { + doiPart = url.substring(AbstractDOIProvider.HTTP_DOI_RESOLVER_URL.length()); + } else if (url.startsWith(AbstractDOIProvider.DXDOI_RESOLVER_URL)) { + doiPart = url.substring(AbstractDOIProvider.DXDOI_RESOLVER_URL.length()); + } else if (url.startsWith(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL)) { + doiPart = url.substring(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL.length()); + } else if (url.startsWith("https://api.datacite.org/dois/")) { + doiPart = url.substring("https://api.datacite.org/dois/".length()); + } else if (url.startsWith("http://api.datacite.org/dois/")) { + doiPart = url.substring("http://api.datacite.org/dois/".length()); + } else if (url.startsWith("https://api.test.datacite.org/dois/")) { + doiPart = url.substring("https://api.test.datacite.org/dois/".length()); + } else if (url.startsWith("http://api.test.datacite.org/dois/")) { + doiPart = url.substring("http://api.test.datacite.org/dois/".length()); + } + + if (doiPart != null) { + return (new UnmanagedDOIProvider()).parsePersistentId(AbstractDOIProvider.DOI_PROTOCOL, doiPart) != null; + } + return false; + } + /** * Parse DataCite XML to extract title and resource type. */ diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java new file mode 100644 index 00000000000..97aeee4598c --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java @@ -0,0 +1,52 @@ +package edu.harvard.iq.dataverse.api.ldn; + +import edu.harvard.iq.dataverse.DatasetServiceBean; +import edu.harvard.iq.dataverse.DataverseRoleServiceBean; +import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; +import edu.harvard.iq.dataverse.UserNotificationServiceBean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; + +public class COARNotifyRelationshipAnnouncementTest { + + private COARNotifyRelationshipAnnouncement handler; + + @BeforeEach + public void setUp() { + DatasetServiceBean datasetService = Mockito.mock(DatasetServiceBean.class); + UserNotificationServiceBean userNotificationService = Mockito.mock(UserNotificationServiceBean.class); + DataverseRoleServiceBean roleService = Mockito.mock(DataverseRoleServiceBean.class); + RoleAssigneeServiceBean roleAssigneeService = Mockito.mock(RoleAssigneeServiceBean.class); + handler = new COARNotifyRelationshipAnnouncement(datasetService, userNotificationService, roleService, roleAssigneeService); + } + + @Test + public void testIsTrustedDataCiteUrl() { + // Trusted DOI resolvers + assertTrue(handler.isTrustedDataCiteUrl("https://doi.org/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("http://doi.org/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("https://dx.doi.org/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("http://dx.doi.org/10.7910/DVN/TJCLKP")); + + // DataCite API + assertTrue(handler.isTrustedDataCiteUrl("https://api.datacite.org/dois/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("http://api.datacite.org/dois/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("https://api.test.datacite.org/dois/10.7910/DVN/TJCLKP")); + assertTrue(handler.isTrustedDataCiteUrl("http://api.test.datacite.org/dois/10.7910/DVN/TJCLKP")); + + // Invalid DOIs + assertFalse(handler.isTrustedDataCiteUrl("https://doi.org/not-a-doi")); + assertFalse(handler.isTrustedDataCiteUrl("https://api.datacite.org/dois/not-a-doi")); + + // Untrusted sources + assertFalse(handler.isTrustedDataCiteUrl("https://example.com/metadata.xml")); + assertFalse(handler.isTrustedDataCiteUrl("https://malicious.org/doi.org/10.1234/5678")); + + // Null and empty + assertFalse(handler.isTrustedDataCiteUrl(null)); + assertFalse(handler.isTrustedDataCiteUrl("")); + } +} From 93f3c95d88a107f62bfea3dd0d9740266dc0ab00 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Wed, 15 Jul 2026 13:59:16 -0400 Subject: [PATCH 2/4] note * not recommended for prod --- doc/sphinx-guides/source/installation/config.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 13eed47b6c4..7ba3789d6c8 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -3926,8 +3926,10 @@ Linked Data Notifications (LDN) Allowed Hosts +++++++++++++++++++++++++++++++++++++++++++++ Dataverse supports receiving LDN notifications via the /api/inbox endpoint. The dataverse.ldn.allowed-hosts allows you to specify the list of host IP addresses from which LDN notifications can be received, or ``*`` to receive messages from anywhere. +Note that since the Inbox endpoint does not require authentication, allowing un-trusted hosts via ``*`` is not recommended for production. Example: ``dataverse.ldn.allowed-hosts=*`` +Example: ``dataverse.ldn.allowed-hosts=172.16.234.56,172.16.234.57`` COAR Notify Relationship Announcement Notify Superusers Only ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ From 85639e5047b79226436620226e050c309daa95e2 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Mon, 27 Jul 2026 12:42:08 -0400 Subject: [PATCH 3/4] updates per review, dropping support for api urls --- .../ldn/COARNotifyRelationshipAnnouncement.java | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java index fefeb23edf8..cfeb034782b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java @@ -12,7 +12,6 @@ import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; import edu.harvard.iq.dataverse.pidproviders.doi.UnmanagedDOIProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.json.JsonLDNamespace; import edu.harvard.iq.dataverse.util.json.JsonLDTerm; import edu.harvard.iq.dataverse.util.json.JsonUtil; @@ -72,7 +71,6 @@ public COARNotifyRelationshipAnnouncement( * Process a COAR Notify Relationship Announcement message. * * @param msgObject The JSON-LD message object - * @return true if the message was successfully processed, false otherwise */ public void processMessage(JsonObject msgObject) { // Extract subject, object, and relationship from the message @@ -195,10 +193,6 @@ private ResourceMetadata retrieveResourceMetadata(String subjectId) { return metadata; } - /** - * Extract DataCite XML URL from Signposting Link headers. - */ - /** * Extract DataCite XML URL from Signposting Link headers. */ @@ -246,6 +240,7 @@ boolean isTrustedDataCiteUrl(String url) { if (url == null || url.isBlank()) { return false; } + url = url.toLowerCase(); String doiPart = null; if (url.startsWith(AbstractDOIProvider.DOI_RESOLVER_URL)) { @@ -256,14 +251,6 @@ boolean isTrustedDataCiteUrl(String url) { doiPart = url.substring(AbstractDOIProvider.DXDOI_RESOLVER_URL.length()); } else if (url.startsWith(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL)) { doiPart = url.substring(AbstractDOIProvider.HTTP_DXDOI_RESOLVER_URL.length()); - } else if (url.startsWith("https://api.datacite.org/dois/")) { - doiPart = url.substring("https://api.datacite.org/dois/".length()); - } else if (url.startsWith("http://api.datacite.org/dois/")) { - doiPart = url.substring("http://api.datacite.org/dois/".length()); - } else if (url.startsWith("https://api.test.datacite.org/dois/")) { - doiPart = url.substring("https://api.test.datacite.org/dois/".length()); - } else if (url.startsWith("http://api.test.datacite.org/dois/")) { - doiPart = url.substring("http://api.test.datacite.org/dois/".length()); } if (doiPart != null) { From 119cf32f6f792b0a9e8f75ed1accf2ad1a977a71 Mon Sep 17 00:00:00 2001 From: Jim Myers Date: Mon, 27 Jul 2026 13:02:35 -0400 Subject: [PATCH 4/4] update test --- .../ldn/COARNotifyRelationshipAnnouncementTest.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java index 97aeee4598c..c0f2de888e8 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncementTest.java @@ -31,15 +31,9 @@ public void testIsTrustedDataCiteUrl() { assertTrue(handler.isTrustedDataCiteUrl("https://dx.doi.org/10.7910/DVN/TJCLKP")); assertTrue(handler.isTrustedDataCiteUrl("http://dx.doi.org/10.7910/DVN/TJCLKP")); - // DataCite API - assertTrue(handler.isTrustedDataCiteUrl("https://api.datacite.org/dois/10.7910/DVN/TJCLKP")); - assertTrue(handler.isTrustedDataCiteUrl("http://api.datacite.org/dois/10.7910/DVN/TJCLKP")); - assertTrue(handler.isTrustedDataCiteUrl("https://api.test.datacite.org/dois/10.7910/DVN/TJCLKP")); - assertTrue(handler.isTrustedDataCiteUrl("http://api.test.datacite.org/dois/10.7910/DVN/TJCLKP")); - - // Invalid DOIs + // Invalid DOI assertFalse(handler.isTrustedDataCiteUrl("https://doi.org/not-a-doi")); - assertFalse(handler.isTrustedDataCiteUrl("https://api.datacite.org/dois/not-a-doi")); + // Untrusted sources assertFalse(handler.isTrustedDataCiteUrl("https://example.com/metadata.xml"));