From c2c4088a76f72359837f8472c50c27b40904dd75 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 6 May 2026 17:55:49 +0200 Subject: [PATCH 01/10] Curation I/O path safety Includes some central validation that other classes can make use of. However, it may be overly restrictive. And we may need to allow multiple separate absolute base paths for each config? (cherry picked from commit dddabe6d2b18895430ff8e16a5f54265eb49bf59) --- .../main/java/org/dspace/curate/Curation.java | 25 +++- .../storage/secure/SecureFileAccess.java | 121 ++++++++++++++++++ dspace/config/modules/curate.cfg | 9 ++ 3 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index a2b4bc482ad8..787d4fd611cc 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -10,12 +10,12 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.time.Instant; import java.util.HashMap; @@ -39,6 +39,7 @@ import org.dspace.handle.service.HandleService; import org.dspace.scripts.DSpaceRunnable; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.storage.secure.SecureFileAccess; import org.dspace.utils.DSpace; /** @@ -114,7 +115,12 @@ private void handleCurationTask(Curator curator) throws IOException, SQLExceptio // load taskFile BufferedReader reader = null; try { - reader = new BufferedReader(new FileReader(this.taskFile)); + String dspaceDir = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("dspace.dir"); + String allowedTaskFileBasePath = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("curate.taskfile.base", dspaceDir); + reader = SecureFileAccess.getBufferedReader(this.taskFile, allowedTaskFileBasePath, + "curation-taskfile", StandardCharsets.UTF_8); while ((taskName = reader.readLine()) != null) { if (verbose) { super.handler.logInfo("Adding task: " + taskName); @@ -190,12 +196,23 @@ private void endScript(long timeRun) throws SQLException { private Curator initCurator() throws FileNotFoundException { Curator curator = new Curator(handler); OutputStream reporterStream; + String dspaceDir = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("dspace.dir"); + String allowedReporterBasePath = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("curate.reporter.base", + dspaceDir + File.separatorChar + "log"); if (null == this.reporter) { - reporterStream = NullOutputStream.NULL_OUTPUT_STREAM; + reporterStream = NullOutputStream.INSTANCE; } else if ("-".equals(this.reporter)) { reporterStream = System.out; } else { - reporterStream = new PrintStream(this.reporter); + try { + reporterStream = new PrintStream( + SecureFileAccess.getOutputStream( + this.reporter, allowedReporterBasePath, "curation-reporter")); + } catch (IOException e) { + throw new FileNotFoundException(e.getLocalizedMessage()); + } } Writer reportWriter = new OutputStreamWriter(reporterStream); curator.setReporter(reportWriter); diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java new file mode 100644 index 000000000000..dbe21520f2fd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -0,0 +1,121 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.storage.secure; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; + +/** + * Decent I/O path validation - not perfect when symlinks are used and we are writing + * as 'toRealPath' check on the resolved path fails for new files + * + * @author Kim Shepherd + */ +public final class SecureFileAccess { + + private SecureFileAccess() {} + + /** + * Validate a given path against an allowed base path. Does not attempt to calculate "real path" + * before validation, as this breaks for new files which don't yet exist. This can make the resulting + * validation still vulnerable to symlink traversal in some cases + * @param file the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static Path validatePathForWrite(String file, String allowedBasePath, String purpose) + throws IOException { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).normalize(); + if (!resolvedPath.startsWith(basePath)) { + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + } + return resolvedPath; + } + + /** + * Validate a given path against an allowed base path. + * More secure than the 'write' variant because we can explicitly resolve links as well. + * + * @param file the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static Path validatePathForRead(String file, String allowedBasePath, String purpose) + throws IOException { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); + if (!resolvedPath.startsWith(basePath)) { + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + } + return resolvedPath; + } + + /** + * Get a buffered reader after validating file path. + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static BufferedReader getBufferedReader(String unvalidatedFile, String allowedBasePath, + String purpose, Charset charset) throws IOException { + if (charset == null) { + charset = StandardCharsets.UTF_8; + } + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePath, purpose); + return Files.newBufferedReader(validatedFile, charset); + } + + /** + * Get an input stream after validating file path. + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static InputStream getInputStream(String unvalidatedFile, String allowedBasePath, String purpose) + throws IOException { + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePath, purpose); + return Files.newInputStream(validatedFile); + + } + + /** + * Get an input stream after validating file path. Adds NOFOLLOW_LINKS link option to + * help ensure some extra safety since new files can't use toRealPath() for link calculation + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static OutputStream getOutputStream(String unvalidatedFile, String allowedBasePath, String purpose) + throws IOException { + Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePath, purpose); + return Files.newOutputStream(validatedFile, LinkOption.NOFOLLOW_LINKS); + } +} diff --git a/dspace/config/modules/curate.cfg b/dspace/config/modules/curate.cfg index 6e75738de543..68c6ea88fed6 100644 --- a/dspace/config/modules/curate.cfg +++ b/dspace/config/modules/curate.cfg @@ -29,3 +29,12 @@ curate.taskqueue.dir = ${dspace.dir}/ctqueues # Maximum amount of redirects set to 0 for none and -1 for unlimited curate.checklinks.max-redirect = 0 + +# allowed base path of curation task files +# it is recommended to restrict this path as much as possible +# so that the DSpace Processes framework may only load files as "tasks" +# from a trusted location +curate.taskfile.base = ${dspace.dir} + +# allowed base path of reporter output. +curate.reporter.base = ${dspace.dir}/log From aee73a3628ba9d93f0614ff2eca4082580330c7b Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sun, 3 May 2026 14:56:24 +0200 Subject: [PATCH 02/10] Restrict LDN message templates to configured path # Conflicts: # dspace-api/src/main/java/org/dspace/core/LDN.java (cherry picked from commit 8e3c640d124f0507e778591868a7ec38bcaefffe) --- .../src/main/java/org/dspace/core/LDN.java | 19 ++++++++++++- .../ldn/action/SendLDNMessageActionIT.java | 27 +++++++++++++++++++ dspace/config/modules/ldn.cfg | 5 ++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java index c0c5690817cf..72f05afad622 100644 --- a/dspace-api/src/main/java/org/dspace/core/LDN.java +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -10,11 +10,14 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import java.io.BufferedReader; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -56,6 +59,14 @@ public class LDN { /** Velocity template for the message*/ private Template template; + /** Allowed base directory for LDN messages / templates **/ + private static final ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + private static final String dspaceDir = configurationService.getProperty("dspace.dir", "/dspace"); + private static final Path allowedTemplateBasePath = Paths.get( + configurationService.getProperty( "ldn.template.path", dspaceDir + File.separatorChar + + "config" + File.separatorChar + "ldn")); + /** * Create a new ldn message. */ @@ -144,8 +155,14 @@ public String generateLDNMessage() { public static LDN getLDNMessage(String ldnMessageFile) throws IOException { StringBuilder contentBuffer = new StringBuilder(); + Path ldnMessagePath = new File(ldnMessageFile).toPath().normalize(); + Path realLdnMessagePath = allowedTemplateBasePath.resolve(ldnMessagePath).normalize(); + if (!realLdnMessagePath.startsWith(allowedTemplateBasePath)) { + throw new IOException("Illegal LDN message path: '" + ldnMessagePath + "'"); + } + try ( - InputStream is = new FileInputStream(ldnMessageFile); + InputStream is = new FileInputStream(realLdnMessagePath.toFile()); InputStreamReader ir = new InputStreamReader(is, "UTF-8"); BufferedReader reader = new BufferedReader(ir); ) { diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java index 73f97b2a6a7c..0be888d92b05 100644 --- a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java +++ b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java @@ -10,10 +10,14 @@ import static org.dspace.app.ldn.action.LDNActionStatus.ABORT; import static org.dspace.app.ldn.action.LDNActionStatus.CONTINUE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.List; @@ -40,6 +44,7 @@ import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; +import org.dspace.core.LDN; import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -232,6 +237,28 @@ public void testLDNMessageConsumerRequestReviewWithInvalidLdnUrl() throws Except response.close(); } + @Test + public void testLDNLegalPath() throws Exception { + try { + // Path traversal will be calculated properly + // but this still ends up at a legal base + LDN message = LDN.getLDNMessage("../../config/ldn/request-review"); + assertNotNull(message); + } catch (IOException e) { + fail("IOException should NOT have been thrown for legal template path: " + e.getMessage()); + } + } + + @Test + public void testLDNIllegalPath() throws Exception { + try { + LDN.getLDNMessage("../modules/dspace.cfg"); + fail("IOException should have been thrown for illegal template path"); + } catch (IOException e) { + assertTrue(e.getMessage().contains("Illegal LDN message path:")); + } + } + @Override @After public void destroy() throws Exception { diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg index 7ed12def20ea..ddb1f56ce52e 100644 --- a/dspace/config/modules/ldn.cfg +++ b/dspace/config/modules/ldn.cfg @@ -40,6 +40,11 @@ ldn.notify.inbox.block-untrusted-ip = true # this is the medatada used to retrieve the relation with external items when sending relationship requests #ldn.notify.relation.metadata = dc.relation +# Allowed base path of LDN templates. Apache Velocity will only +# load templates that begin with this base path. +# Default is ${dspace.dir}/config/ldn +#ldn.template.path = ${dspace.dir}/config/ldn + # EMAIL CONFIGURATION # Supported values for actionSendFilter are: From a734a55d87eba9bcdf5d21ecbc86a8dc5e5ebb1a Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 16 May 2026 16:41:05 +0200 Subject: [PATCH 03/10] Use new SecureFileAccess validator for LDN (cherry picked from commit 83bbb3588c2fa343577b210c1fec23118a822f2a) # Conflicts: # dspace-api/src/main/java/org/dspace/core/LDN.java --- .../src/main/java/org/dspace/core/LDN.java | 22 +++--- .../main/java/org/dspace/curate/Curation.java | 17 +++-- .../storage/secure/SecureFileAccess.java | 74 +++++++++++-------- dspace/config/modules/curate.cfg | 5 +- dspace/config/modules/ldn.cfg | 1 + 5 files changed, 66 insertions(+), 53 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java index 72f05afad622..de788de83f23 100644 --- a/dspace-api/src/main/java/org/dspace/core/LDN.java +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -11,14 +11,12 @@ import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -33,6 +31,9 @@ import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; import org.apache.velocity.runtime.resource.util.StringResourceRepository; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.storage.secure.SecureFileAccess; /** * Class representing an LDN message json @@ -63,9 +64,8 @@ public class LDN { private static final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); private static final String dspaceDir = configurationService.getProperty("dspace.dir", "/dspace"); - private static final Path allowedTemplateBasePath = Paths.get( - configurationService.getProperty( "ldn.template.path", dspaceDir + File.separatorChar - + "config" + File.separatorChar + "ldn")); + private static final String[] DEFAULT_TEMPLATE_PATHS = new String[]{ + dspaceDir + File.separatorChar + "config" + File.separatorChar + "ldn"}; /** * Create a new ldn message. @@ -155,14 +155,10 @@ public String generateLDNMessage() { public static LDN getLDNMessage(String ldnMessageFile) throws IOException { StringBuilder contentBuffer = new StringBuilder(); - Path ldnMessagePath = new File(ldnMessageFile).toPath().normalize(); - Path realLdnMessagePath = allowedTemplateBasePath.resolve(ldnMessagePath).normalize(); - if (!realLdnMessagePath.startsWith(allowedTemplateBasePath)) { - throw new IOException("Illegal LDN message path: '" + ldnMessagePath + "'"); - } - + List allowedBasePaths = Arrays.stream(configurationService + .getArrayProperty("ldn.template.path", DEFAULT_TEMPLATE_PATHS)).toList(); try ( - InputStream is = new FileInputStream(realLdnMessagePath.toFile()); + InputStream is = SecureFileAccess.getInputStream(ldnMessageFile, allowedBasePaths, "ldn"); InputStreamReader ir = new InputStreamReader(is, "UTF-8"); BufferedReader reader = new BufferedReader(ir); ) { diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index 787d4fd611cc..fb82f7b0faa3 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -18,8 +18,10 @@ import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.time.Instant; +import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -117,8 +119,10 @@ private void handleCurationTask(Curator curator) throws IOException, SQLExceptio try { String dspaceDir = DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("dspace.dir"); - String allowedTaskFileBasePath = DSpaceServicesFactory.getInstance() - .getConfigurationService().getProperty("curate.taskfile.base", dspaceDir); + List allowedTaskFileBasePath = Arrays.stream( + DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("curate.taskfile.base", new String[]{dspaceDir}) + ).toList(); reader = SecureFileAccess.getBufferedReader(this.taskFile, allowedTaskFileBasePath, "curation-taskfile", StandardCharsets.UTF_8); while ((taskName = reader.readLine()) != null) { @@ -198,9 +202,10 @@ private Curator initCurator() throws FileNotFoundException { OutputStream reporterStream; String dspaceDir = DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("dspace.dir"); - String allowedReporterBasePath = DSpaceServicesFactory.getInstance() - .getConfigurationService().getProperty("curate.reporter.base", - dspaceDir + File.separatorChar + "log"); + List allowedReporterBasePaths = Arrays.stream( + DSpaceServicesFactory.getInstance() + .getConfigurationService().getArrayProperty("curate.reporter.base", + new String[]{dspaceDir + File.separatorChar + "log"})).toList(); if (null == this.reporter) { reporterStream = NullOutputStream.INSTANCE; } else if ("-".equals(this.reporter)) { @@ -209,7 +214,7 @@ private Curator initCurator() throws FileNotFoundException { try { reporterStream = new PrintStream( SecureFileAccess.getOutputStream( - this.reporter, allowedReporterBasePath, "curation-reporter")); + this.reporter, allowedReporterBasePaths, "curation-reporter")); } catch (IOException e) { throw new FileNotFoundException(e.getLocalizedMessage()); } diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java index dbe21520f2fd..2a902ddf5ea6 100644 --- a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -16,6 +16,7 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.List; /** * Decent I/O path validation - not perfect when symlinks are used and we are writing @@ -32,23 +33,28 @@ private SecureFileAccess() {} * before validation, as this breaks for new files which don't yet exist. This can make the resulting * validation still vulnerable to symlink traversal in some cases * @param file the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths list of allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static Path validatePathForWrite(String file, String allowedBasePath, String purpose) + public static Path validatePathForWrite(String file, List allowedBasePaths, String purpose) throws IOException { - Path basePath = Path.of(allowedBasePath) - .toRealPath() - .normalize(); - if (basePath == null) { - throw new IOException("Null base path can not be provided for validation"); - } - Path resolvedPath = basePath.resolve(file).normalize(); - if (!resolvedPath.startsWith(basePath)) { - throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + for (String allowedBasePath : allowedBasePaths) { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).normalize(); + if (resolvedPath.startsWith(basePath)) { + return resolvedPath; + } } - return resolvedPath; + + // If no valid path was resolved and returned by now + // we raise an exception and treat this as illegal access + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); } /** @@ -60,47 +66,51 @@ public static Path validatePathForWrite(String file, String allowedBasePath, Str * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static Path validatePathForRead(String file, String allowedBasePath, String purpose) + public static Path validatePathForRead(String file, List allowedBasePaths, String purpose) throws IOException { - Path basePath = Path.of(allowedBasePath) - .toRealPath() - .normalize(); - if (basePath == null) { - throw new IOException("Null base path can not be provided for validation"); - } - Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); - if (!resolvedPath.startsWith(basePath)) { - throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + for (String allowedBasePath : allowedBasePaths) { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); + if (resolvedPath.startsWith(basePath)) { + return resolvedPath; + } } - return resolvedPath; + // If no valid path was resolved and returned by now + // we raise an exception and treat this as illegal access + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); } /** * Get a buffered reader after validating file path. * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static BufferedReader getBufferedReader(String unvalidatedFile, String allowedBasePath, + public static BufferedReader getBufferedReader(String unvalidatedFile, List allowedBasePaths, String purpose, Charset charset) throws IOException { if (charset == null) { charset = StandardCharsets.UTF_8; } - Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePath, purpose); + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePaths, purpose); return Files.newBufferedReader(validatedFile, charset); } /** * Get an input stream after validating file path. * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static InputStream getInputStream(String unvalidatedFile, String allowedBasePath, String purpose) + public static InputStream getInputStream(String unvalidatedFile, List allowedBasePaths, String purpose) throws IOException { - Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePath, purpose); + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePaths, purpose); return Files.newInputStream(validatedFile); } @@ -109,13 +119,13 @@ public static InputStream getInputStream(String unvalidatedFile, String allowedB * Get an input stream after validating file path. Adds NOFOLLOW_LINKS link option to * help ensure some extra safety since new files can't use toRealPath() for link calculation * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static OutputStream getOutputStream(String unvalidatedFile, String allowedBasePath, String purpose) + public static OutputStream getOutputStream(String unvalidatedFile, List allowedBasePaths, String purpose) throws IOException { - Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePath, purpose); + Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePaths, purpose); return Files.newOutputStream(validatedFile, LinkOption.NOFOLLOW_LINKS); } } diff --git a/dspace/config/modules/curate.cfg b/dspace/config/modules/curate.cfg index 68c6ea88fed6..26938893b25c 100644 --- a/dspace/config/modules/curate.cfg +++ b/dspace/config/modules/curate.cfg @@ -30,10 +30,11 @@ curate.taskqueue.dir = ${dspace.dir}/ctqueues # Maximum amount of redirects set to 0 for none and -1 for unlimited curate.checklinks.max-redirect = 0 -# allowed base path of curation task files +# allowed base path(s) of curation task files # it is recommended to restrict this path as much as possible # so that the DSpace Processes framework may only load files as "tasks" -# from a trusted location +# from a trusted location. For multiple paths, repeat this configuration +# property for each trusted path curate.taskfile.base = ${dspace.dir} # allowed base path of reporter output. diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg index ddb1f56ce52e..a8ff1b9fc45e 100644 --- a/dspace/config/modules/ldn.cfg +++ b/dspace/config/modules/ldn.cfg @@ -42,6 +42,7 @@ ldn.notify.inbox.block-untrusted-ip = true # Allowed base path of LDN templates. Apache Velocity will only # load templates that begin with this base path. +# You can repeat this parameter to allow multiple base paths. # Default is ${dspace.dir}/config/ldn #ldn.template.path = ${dspace.dir}/config/ldn From 128abe85ecf46deefe7363e2ab2932d9de95e425 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 16 May 2026 17:08:59 +0200 Subject: [PATCH 04/10] Update LDN file path test (cherry picked from commit 8084c92572d3917de5e4126b65e38ad6538605d9) --- .../java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java index 0be888d92b05..1a04d795684c 100644 --- a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java +++ b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java @@ -255,7 +255,7 @@ public void testLDNIllegalPath() throws Exception { LDN.getLDNMessage("../modules/dspace.cfg"); fail("IOException should have been thrown for illegal template path"); } catch (IOException e) { - assertTrue(e.getMessage().contains("Illegal LDN message path:")); + assertTrue(e.getMessage().contains("Illegal file path attempted for I/O (ldn):")); } } From 1661ef0eedb9c07aaccd14f8503f461b9909cff7 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 26 May 2026 14:06:14 +0200 Subject: [PATCH 05/10] Move curation -r reporter param to CLI only (cherry picked from commit 572f952301ff25ab7813331e8b55cd409ddc254f) --- .../java/org/dspace/curate/CurationCliScriptConfiguration.java | 3 +++ .../src/main/java/org/dspace/curate/CurationClientOptions.java | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java index eaa04f477829..07684f3643d0 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java @@ -20,6 +20,9 @@ public Options getOptions() { options = super.getOptions(); options.addOption("e", "eperson", true, "email address of curating eperson"); options.getOption("e").setRequired(true); + options.addOption("r", "reporter", true, + "relative or absolute path to the desired report file. Use '-' to report to console. If absent, no " + + "reporting"); return options; } } diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java index 8ec0f14697c0..a8a3d358ce67 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java @@ -59,9 +59,6 @@ protected static Options constructOptions() { "Id (handle) of object to perform task on, or 'all' to perform on whole repository"); options.addOption("p", "parameter", true, "a task parameter 'NAME=VALUE'"); options.addOption("q", "queue", true, "name of task queue to process"); - options.addOption("r", "reporter", true, - "relative or absolute path to the desired report file. Use '-' to report to console. If absent, no " + - "reporting"); options.addOption("s", "scope", true, "transaction scope to impose: use 'object', 'curation', or 'open'. If absent, 'open' applies"); options.addOption("v", "verbose", false, "report activity to stdout"); From 2250fd9e436d247795cdd3f0a222642763be1741 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 26 May 2026 14:30:00 +0200 Subject: [PATCH 06/10] Improve SecureFileAccess handling (cherry picked from commit 0be7a778cb6e0f7c74c36daa34a1bc55ee007b53) --- .../org/dspace/storage/secure/SecureFileAccess.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java index 2a902ddf5ea6..073604a9bdf5 100644 --- a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -14,7 +14,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.List; @@ -62,7 +61,7 @@ public static Path validatePathForWrite(String file, List allowedBasePat * More secure than the 'write' variant because we can explicitly resolve links as well. * * @param file the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ @@ -72,9 +71,6 @@ public static Path validatePathForRead(String file, List allowedBasePath Path basePath = Path.of(allowedBasePath) .toRealPath() .normalize(); - if (basePath == null) { - throw new IOException("Null base path can not be provided for validation"); - } Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); if (resolvedPath.startsWith(basePath)) { return resolvedPath; @@ -116,8 +112,8 @@ public static InputStream getInputStream(String unvalidatedFile, List al } /** - * Get an input stream after validating file path. Adds NOFOLLOW_LINKS link option to - * help ensure some extra safety since new files can't use toRealPath() for link calculation + * Get an output stream after validating file path. New files can't use toRealPath() for link calculation so + * there is a bit of a trade-off in allowing some symlink traversal to occur * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection @@ -126,6 +122,6 @@ public static InputStream getInputStream(String unvalidatedFile, List al public static OutputStream getOutputStream(String unvalidatedFile, List allowedBasePaths, String purpose) throws IOException { Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePaths, purpose); - return Files.newOutputStream(validatedFile, LinkOption.NOFOLLOW_LINKS); + return Files.newOutputStream(validatedFile); } } From d7226558008d00d561925e9dfc906d83331fa056 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 26 May 2026 18:48:10 +0200 Subject: [PATCH 07/10] Require abs path in SecureFileAccess, calc in callers (cherry picked from commit e9cca8e41f3a5e56f5f07d15a404201c699aff34) --- .../src/main/java/org/dspace/core/LDN.java | 12 +++-- .../main/java/org/dspace/curate/Curation.java | 8 +++- .../storage/secure/SecureFileAccess.java | 44 ++++++++++++++++++- .../ldn/action/SendLDNMessageActionIT.java | 7 ++- dspace/config/modules/ldn.cfg | 7 ++- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java index de788de83f23..19be26c586c9 100644 --- a/dspace-api/src/main/java/org/dspace/core/LDN.java +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -155,10 +155,16 @@ public String generateLDNMessage() { public static LDN getLDNMessage(String ldnMessageFile) throws IOException { StringBuilder contentBuffer = new StringBuilder(); - List allowedBasePaths = Arrays.stream(configurationService - .getArrayProperty("ldn.template.path", DEFAULT_TEMPLATE_PATHS)).toList(); + List allowedBasePaths = List.of( + Arrays.stream(configurationService + .getArrayProperty("ldn.template.path", DEFAULT_TEMPLATE_PATHS)) + .findFirst() + .orElseThrow(() -> new IOException("No LDN template path configured")) + ); + String ldnFilePath = SecureFileAccess.calculateAbsolutePathUsingBaseDir(ldnMessageFile, + allowedBasePaths.get(0)); try ( - InputStream is = SecureFileAccess.getInputStream(ldnMessageFile, allowedBasePaths, "ldn"); + InputStream is = SecureFileAccess.getInputStream(ldnFilePath, allowedBasePaths, "ldn"); InputStreamReader ir = new InputStreamReader(is, "UTF-8"); BufferedReader reader = new BufferedReader(ir); ) { diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index fb82f7b0faa3..a5c1f1c5d2ad 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -116,6 +116,8 @@ private void handleCurationTask(Curator curator) throws IOException, SQLExceptio } else if (commandLine.hasOption('T')) { // load taskFile BufferedReader reader = null; + // in this case, Curation CLI expects to calculate the -T parameter from the user's current working dir + String taskFilePath = SecureFileAccess.calculateAbsolutePathUsingCwd(this.taskFile); try { String dspaceDir = DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("dspace.dir"); @@ -123,7 +125,7 @@ private void handleCurationTask(Curator curator) throws IOException, SQLExceptio DSpaceServicesFactory.getInstance().getConfigurationService() .getArrayProperty("curate.taskfile.base", new String[]{dspaceDir}) ).toList(); - reader = SecureFileAccess.getBufferedReader(this.taskFile, allowedTaskFileBasePath, + reader = SecureFileAccess.getBufferedReader(taskFilePath, allowedTaskFileBasePath, "curation-taskfile", StandardCharsets.UTF_8); while ((taskName = reader.readLine()) != null) { if (verbose) { @@ -211,10 +213,12 @@ private Curator initCurator() throws FileNotFoundException { } else if ("-".equals(this.reporter)) { reporterStream = System.out; } else { + // Reporter param comes from CLI execution. Calculate abs path from user's current working dir + String reporterFilePath = SecureFileAccess.calculateAbsolutePathUsingCwd(this.reporter); try { reporterStream = new PrintStream( SecureFileAccess.getOutputStream( - this.reporter, allowedReporterBasePaths, "curation-reporter")); + reporterFilePath, allowedReporterBasePaths, "curation-reporter")); } catch (IOException e) { throw new FileNotFoundException(e.getLocalizedMessage()); } diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java index 073604a9bdf5..3d70831b0b9f 100644 --- a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -32,12 +32,18 @@ private SecureFileAccess() {} * before validation, as this breaks for new files which don't yet exist. This can make the resulting * validation still vulnerable to symlink traversal in some cases * @param file the unvalidated file, usually derived from user input or configuration + * This MUST be an absolute path, and the caller is expected to calculate it based on best + * context (e.g. configured base path, CWD, dspace.dir, and so on) * @param allowedBasePaths list of allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ public static Path validatePathForWrite(String file, List allowedBasePaths, String purpose) throws IOException { + Path filePath = Path.of(file); + if (!filePath.isAbsolute()) { + throw new IOException("Absolute path required for I/O (%s): %s".formatted(purpose, file)); + } for (String allowedBasePath : allowedBasePaths) { Path basePath = Path.of(allowedBasePath) .toRealPath() @@ -50,7 +56,7 @@ public static Path validatePathForWrite(String file, List allowedBasePat return resolvedPath; } } - + // If no valid path was resolved and returned by now // we raise an exception and treat this as illegal access throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); @@ -61,12 +67,18 @@ public static Path validatePathForWrite(String file, List allowedBasePat * More secure than the 'write' variant because we can explicitly resolve links as well. * * @param file the unvalidated file, usually derived from user input or configuration + * This MUST be an absolute path, and the caller is expected to calculate it based on best + * context (e.g. configured base path, CWD, dspace.dir, and so on) * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ public static Path validatePathForRead(String file, List allowedBasePaths, String purpose) throws IOException { + Path filePath = Path.of(file); + if (!filePath.isAbsolute()) { + throw new IOException("Absolute path required for I/O (%s): %s".formatted(purpose, file)); + } for (String allowedBasePath : allowedBasePaths) { Path basePath = Path.of(allowedBasePath) .toRealPath() @@ -124,4 +136,34 @@ public static OutputStream getOutputStream(String unvalidatedFile, List Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePaths, purpose); return Files.newOutputStream(validatedFile); } + + /** + * Calculate an absolute path (if not already absolute) using current working dir as a root + * for relative file paths + * @param file the relative or absolute file given as input + * @return absolute path calculated from file and cwd + */ + public static String calculateAbsolutePathUsingCwd(String file) { + String filePath = file; + Path path = Path.of(filePath); + if (!path.isAbsolute()) { + filePath = Path.of("").toAbsolutePath().resolve(path).normalize().toString(); + } + return filePath; + } + + /** + * Calculate an absolute path (if not already absolute) using a given base dir as a root + * for relative file paths + * @param file the relative or absolute file given as input + * @return absolute path calculated from file and base dir + */ + public static String calculateAbsolutePathUsingBaseDir(String file, String baseDir) { + String filePath = file; + Path path = Path.of(filePath); + if (!path.isAbsolute()) { + filePath = Path.of(baseDir).toAbsolutePath().resolve(path).normalize().toString(); + } + return filePath; + } } diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java index 1a04d795684c..bec0de59619c 100644 --- a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java +++ b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.sql.SQLException; import java.util.List; @@ -252,7 +253,11 @@ public void testLDNLegalPath() throws Exception { @Test public void testLDNIllegalPath() throws Exception { try { - LDN.getLDNMessage("../modules/dspace.cfg"); + String badAbsolutePath = Path.of(configurationService.getProperty("dspace.dir")) + .resolve("config/dspace.cfg") + .toAbsolutePath() + .toString(); + LDN.getLDNMessage(badAbsolutePath); fail("IOException should have been thrown for illegal template path"); } catch (IOException e) { assertTrue(e.getMessage().contains("Illegal file path attempted for I/O (ldn):")); diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg index a8ff1b9fc45e..7093cd314937 100644 --- a/dspace/config/modules/ldn.cfg +++ b/dspace/config/modules/ldn.cfg @@ -40,13 +40,12 @@ ldn.notify.inbox.block-untrusted-ip = true # this is the medatada used to retrieve the relation with external items when sending relationship requests #ldn.notify.relation.metadata = dc.relation -# Allowed base path of LDN templates. Apache Velocity will only -# load templates that begin with this base path. -# You can repeat this parameter to allow multiple base paths. +# Base path of LDN templates. Apache Velocity will only +# load templates that begin with this base path, and relative paths will use this as a base when +# calculating the absolute path. This is not repeatable, multiple paths will be ignored beyond the first. # Default is ${dspace.dir}/config/ldn #ldn.template.path = ${dspace.dir}/config/ldn - # EMAIL CONFIGURATION # Supported values for actionSendFilter are: # single email, "GROUP:" or "SUBMITTER" From cbcc4d39eaf89928bd9856456f2c319287e2e108 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 06:40:16 +0200 Subject: [PATCH 08/10] Comment out default curation dir properties (cherry picked from commit f97b34bcde50a5261ade5bba1f4e9000b60660d2) --- dspace/config/modules/curate.cfg | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dspace/config/modules/curate.cfg b/dspace/config/modules/curate.cfg index 26938893b25c..dad40d615454 100644 --- a/dspace/config/modules/curate.cfg +++ b/dspace/config/modules/curate.cfg @@ -35,7 +35,9 @@ curate.checklinks.max-redirect = 0 # so that the DSpace Processes framework may only load files as "tasks" # from a trusted location. For multiple paths, repeat this configuration # property for each trusted path -curate.taskfile.base = ${dspace.dir} +# Default: ${dspace.dir} +#curate.taskfile.base = ${dspace.dir} # allowed base path of reporter output. -curate.reporter.base = ${dspace.dir}/log +# Default: ${dspace.dir}/log +#curate.reporter.base = ${dspace.dir}/log From b0130d039efe5b8aef0d0afdaec2a86dc45af853 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 06:43:22 +0200 Subject: [PATCH 09/10] Move taskfile -T option to CLI script config only (cherry picked from commit 00e4979a60fd69adbf4a7476926701ef59207ce7) --- .../org/dspace/curate/CurationCliScriptConfiguration.java | 1 + .../main/java/org/dspace/curate/CurationClientOptions.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java index 07684f3643d0..925bd4f2d232 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java @@ -23,6 +23,7 @@ public Options getOptions() { options.addOption("r", "reporter", true, "relative or absolute path to the desired report file. Use '-' to report to console. If absent, no " + "reporting"); + options.addOption("T", "taskfile", true, "file containing curation task names"); return options; } } diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java index a8a3d358ce67..03ad2f34b230 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java @@ -31,7 +31,8 @@ public enum CurationClientOptions { /** * This method resolves the CommandLine parameters to figure out which action the curation script should perform * - * @param commandLine The relevant CommandLine for the curation script + * @param commandLine The relevant CommandLine for the curation script. Note that -T is passed only + * from CurationCliScriptConfig and is not accessible from UI processes * @return The curation option to be ran, parsed from the CommandLine */ protected static CurationClientOptions getClientOption(CommandLine commandLine) { @@ -54,7 +55,6 @@ protected static Options constructOptions() { Options options = new Options(); options.addOption("t", "task", true, "curation task name; options: " + getTaskOptions()); - options.addOption("T", "taskfile", true, "file containing curation task names"); options.addOption("i", "id", true, "Id (handle) of object to perform task on, or 'all' to perform on whole repository"); options.addOption("p", "parameter", true, "a task parameter 'NAME=VALUE'"); From db7337c205ac301b80112ba39faa3d794771e207 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 14:38:00 +0200 Subject: [PATCH 10/10] Ignore CurationScriptIT -T taskFile tests, to rewrite w/ CLI (cherry picked from commit 6437472b8277b9aa815dd71e14b499ba7515f87d) --- .../src/test/java/org/dspace/curate/CurationScriptIT.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java b/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java index 9a1392117d66..37d5e3878955 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java @@ -49,6 +49,7 @@ import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.service.ScriptService; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -217,6 +218,7 @@ public void curateScript_InvalidScope() throws Exception { .andExpect(status().isBadRequest()); } + @Ignore @Test public void curateScript_InvalidTaskFile() throws Exception { String token = getAuthToken(admin.getEmail(), password); @@ -289,6 +291,7 @@ public void curateScript_validRequest_Task() throws Exception { } } + @Ignore @Test public void curateScript_validRequest_TaskFile() throws Exception { context.turnOffAuthorisationSystem();