[MNT-25375] Unauthenticated Remote Code Execution vulnerability through Argument Injection (#1145)

* [MNT-25375] WIP

* [MNT-25375] Spotless

* [MNT-25375] Rewritten test, spotless, updated license year

* [MNT-25375] Update imagemagick in aio

* [MNT-25375] Update imagemagick to 7.1.2-6

* [MNT-25375] Updating

* [MNT-25375] Restored test

* [MNT-25375] Added transform.core.imagemagick.commandOptions.enabled and IMAGEMAGICK_COMMAND_OPTIONS_ENABLED to enable deprecated unsecure functionality on demand

* [MNT-25375] Spotless

* [MNT-25375] Missing header

* [MNT-25375] Review fixes

* [MNT-25375] Review fixes
This commit is contained in:
cezary-witkowski
2025-10-20 12:56:09 +02:00
committed by GitHub
parent 70c0a5c39b
commit 01e11e7eac
13 changed files with 435 additions and 173 deletions
+3 -3
View File
@@ -12,9 +12,9 @@ ARG EXIFTOOL_VERSION=12.25
ARG EXIFTOOL_FOLDER=Image-ExifTool-${EXIFTOOL_VERSION}
ARG EXIFTOOL_URL=https://nexus.alfresco.com/nexus/service/local/repositories/thirdparty/org/exiftool/image-exiftool/${EXIFTOOL_VERSION}/image-exiftool-${EXIFTOOL_VERSION}.tgz
ARG IMAGEMAGICK_VERSION=7.1.0-16
ENV IMAGEMAGICK_RPM_URL=https://nexus.alfresco.com/nexus/service/local/repositories/thirdparty/org/imagemagick/imagemagick-distribution/${IMAGEMAGICK_VERSION}-ci-10/imagemagick-distribution-${IMAGEMAGICK_VERSION}-ci-10
ENV IMAGEMAGICK_DEP_RPM_URL=https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
ARG IMAGEMAGICK_VERSION=7.1.2-6
ENV IMAGEMAGICK_RPM_URL=https://nexus.alfresco.com/nexus/service/local/repositories/thirdparty/org/imagemagick/imagemagick-distribution/${IMAGEMAGICK_VERSION}-ci-1/imagemagick-distribution-${IMAGEMAGICK_VERSION}-ci-1
ENV IMAGEMAGICK_DEP_RPM_URL=https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
ARG LIBREOFFICE_VERSION=7.2.5
ENV LIBREOFFICE_RPM_URL=https://nexus.alfresco.com/nexus/service/local/repositories/thirdparty/org/libreoffice/libreoffice-dist/${LIBREOFFICE_VERSION}/libreoffice-dist-${LIBREOFFICE_VERSION}-linux.gz
@@ -13,11 +13,13 @@ transform:
templateProfileDir: ${LIBREOFFICE_TEMPLATE_PROFILE_DIR:}
isEnabled: ${LIBREOFFICE_IS_ENABLED:true}
imagemagick:
root: ${IMAGEMAGICK_ROOT:/usr/lib64/ImageMagick-7.0.10}
dyn: ${IMAGEMAGICK_DYN:/usr/lib64/ImageMagick-7.0.10/lib}
root: ${IMAGEMAGICK_ROOT:/usr/lib64/ImageMagick-7.1.2}
dyn: ${IMAGEMAGICK_DYN:/usr/lib64/ImageMagick-7.1.2/lib}
exe: ${IMAGEMAGICK_EXE:/usr/bin/convert}
coders: ${IMAGEMAGICK_CODERS:}
config: ${IMAGEMAGICK_CONFIG:}
commandOptions:
enabled: ${IMAGEMAGICK_COMMAND_OPTIONS_ENABLED:false}
tika:
pdfBox:
notExtractBookmarksTextDefault: ${PDFBOX_NOTEXTRACTBOOKMARKS_DEFAULT:false}
@@ -28,4 +30,4 @@ transform:
pdfBox:
defaultFont: ${MISC_PDFBOX_DEFAULT_FONT:NotoSans-Regular}
htmlOptions:
collapseHtml: ${MISC_HTML_COLLAPSE:true}
collapseHtml: ${MISC_HTML_COLLAPSE:true}
+3 -3
View File
@@ -6,10 +6,10 @@
FROM alfresco/alfresco-base-java:jre17-rockylinux9@sha256:f98833508b7be8c4b44a25450f9faac44cacfdc075f2295e02836b93fd05bb9c
# For other ImageMagick versions please look at https://github.com/Alfresco/imagemagick-build tags
ARG IMAGEMAGICK_VERSION=7.1.0-16
ARG IMAGEMAGICK_VERSION=7.1.2-6
ENV IMAGEMAGICK_RPM_URL=https://nexus.alfresco.com/nexus/service/local/repositories/thirdparty/org/imagemagick/imagemagick-distribution/${IMAGEMAGICK_VERSION}-ci-10/imagemagick-distribution-${IMAGEMAGICK_VERSION}-ci-10
ENV IMAGEMAGICK_DEP_RPM_URL=https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
ENV IMAGEMAGICK_RPM_URL=https://nexus.alfresco.com/nexus/service/local/repositories/thirdparty/org/imagemagick/imagemagick-distribution/${IMAGEMAGICK_VERSION}-ci-1/imagemagick-distribution-${IMAGEMAGICK_VERSION}-ci-1
ENV IMAGEMAGICK_DEP_RPM_URL=https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
ENV JAVA_OPTS=""
# Set default user information
@@ -2,7 +2,7 @@
* #%L
* Alfresco Transform Core
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
@@ -48,8 +48,6 @@ public final class ImageMagickOptionsBuilder
private static final List<String> GRAVITY_VALUES = ImmutableList.of("North", "NorthEast",
"East", "SouthEast", "South", "SouthWest", "West", "NorthWest", "Center");
private Integer startPage;
private Integer endPage;
private Boolean alphaRemove;
private Boolean autoOrient;
private String cropGravity;
@@ -69,28 +67,6 @@ public final class ImageMagickOptionsBuilder
private ImageMagickOptionsBuilder()
{}
public ImageMagickOptionsBuilder withStartPage(final String startPage)
{
return withStartPage(stringToInteger(startPage));
}
public ImageMagickOptionsBuilder withStartPage(final Integer startPage)
{
this.startPage = startPage;
return this;
}
public ImageMagickOptionsBuilder withEndPage(final String endPage)
{
return withEndPage(stringToInteger(endPage));
}
public ImageMagickOptionsBuilder withEndPage(final Integer endPage)
{
this.endPage = endPage;
return this;
}
public ImageMagickOptionsBuilder withAlphaRemove(final String alphaRemove)
{
return withAlphaRemove(stringToBoolean(alphaRemove));
@@ -353,7 +329,7 @@ public final class ImageMagickOptionsBuilder
return (commandOptions == null || "".equals(
commandOptions.trim()) ? "" : commandOptions + ' ') +
args.toString();
args;
}
public static ImageMagickOptionsBuilder builder()
@@ -0,0 +1,120 @@
/*
* #%L
* Alfresco Transform Core
* %%
* Copyright (C) 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.transform.imagemagick.transformers;
import java.util.HashMap;
import java.util.Map;
import jakarta.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.alfresco.transform.base.executors.AbstractCommandExecutor;
import org.alfresco.transform.base.executors.RuntimeExec;
@Component
public class ImageMagickCommandExecutor extends AbstractCommandExecutor
{
@Value("${transform.core.imagemagick.exe}")
private String exe;
@Value("${transform.core.imagemagick.dyn}")
private String dyn;
@Value("${transform.core.imagemagick.root}")
private String root;
// Not currently used, but may be again in the future if we need an ImageMagick extension
@Value("${transform.core.imagemagick.coders}")
private String coders;
@Value("${transform.core.imagemagick.config}")
private String config;
@PostConstruct
private void createCommands()
{
if (StringUtils.isEmpty(exe))
{
throw new IllegalArgumentException("ImageMagickTransformer IMAGEMAGICK_EXE variable cannot be null or empty");
}
if (StringUtils.isEmpty(dyn))
{
throw new IllegalArgumentException("ImageMagickTransformer IMAGEMAGICK_DYN variable cannot be null or empty");
}
if (StringUtils.isEmpty(root))
{
throw new IllegalArgumentException("ImageMagickTransformer IMAGEMAGICK_ROOT variable cannot be null or empty");
}
super.transformCommand = createTransformCommand();
super.checkCommand = createCheckCommand();
}
@Override
protected RuntimeExec createTransformCommand()
{
RuntimeExec runtimeExec = new RuntimeExec();
Map<String, String[]> commandsAndArguments = new HashMap<>();
commandsAndArguments.put(".*",
new String[]{exe, "${source}", "SPLIT:${options}", "-strip", "-quiet", "${target}"});
runtimeExec.setCommandsAndArguments(commandsAndArguments);
Map<String, String> processProperties = new HashMap<>();
processProperties.put("MAGICK_HOME", root);
processProperties.put("DYLD_FALLBACK_LIBRARY_PATH", dyn);
processProperties.put("LD_LIBRARY_PATH", dyn);
// Optional properties (see also https://imagemagick.org/script/resources.php#environment)
if (coders != null && !coders.isBlank())
{
processProperties.put("MAGICK_CODER_MODULE_PATH", coders);
}
if (config != null && !config.isBlank())
{
processProperties.put("MAGICK_CONFIGURE_PATH", config);
}
runtimeExec.setProcessProperties(processProperties);
Map<String, String> defaultProperties = new HashMap<>();
defaultProperties.put("options", null);
runtimeExec.setDefaultProperties(defaultProperties);
runtimeExec.setErrorCodes(
"1,2,255,400,405,410,415,420,425,430,435,440,450,455,460,465,470,475,480,485,490,495,499,700,705,710,715,720,725,730,735,740,750,755,760,765,770,775,780,785,790,795,799");
return runtimeExec;
}
@Override
protected RuntimeExec createCheckCommand()
{
RuntimeExec runtimeExec = new RuntimeExec();
Map<String, String[]> commandsAndArguments = new HashMap<>();
commandsAndArguments.put(".*", new String[]{exe, "-version"});
runtimeExec.setCommandsAndArguments(commandsAndArguments);
return runtimeExec;
}
}
@@ -0,0 +1,61 @@
/*
* #%L
* Alfresco Transform Core
* %%
* Copyright (C) 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.transform.imagemagick.transformers;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ImageMagickCommandOptions
{
private static final Logger LOG = LoggerFactory.getLogger(ImageMagickCommandOptions.class);
@Value("${transform.core.imagemagick.commandOptions.enabled}")
private boolean commandOptionsEnabled;
@PostConstruct
void init()
{
if (commandOptionsEnabled)
{
LOG.warn("Parameter commandOptions is enabled. It is deprecated and due to security concerns, it will be removed in the future.");
}
else
{
LOG.info("Parameter commandOptions is disabled.");
}
}
public boolean isCommandOptionsEnabled()
{
return commandOptionsEnabled;
}
}
@@ -2,7 +2,7 @@
* #%L
* Alfresco Transform Core
* %%
* Copyright (C) 2005 - 2023 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
@@ -26,13 +26,7 @@
*/
package org.alfresco.transform.imagemagick.transformers;
import static org.alfresco.transform.base.util.Util.stringToInteger;
import static org.alfresco.transform.base.util.Util.stringToLong;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_BMP;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JP2;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_PNG;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_XWD;
import static org.alfresco.transform.common.RequestParamMap.ALLOW_ENLARGEMENT;
import static org.alfresco.transform.common.RequestParamMap.ALPHA_REMOVE;
import static org.alfresco.transform.common.RequestParamMap.AUTO_ORIENT;
@@ -43,71 +37,39 @@ import static org.alfresco.transform.common.RequestParamMap.CROP_PERCENTAGE;
import static org.alfresco.transform.common.RequestParamMap.CROP_WIDTH;
import static org.alfresco.transform.common.RequestParamMap.CROP_X_OFFSET;
import static org.alfresco.transform.common.RequestParamMap.CROP_Y_OFFSET;
import static org.alfresco.transform.common.RequestParamMap.END_PAGE;
import static org.alfresco.transform.common.RequestParamMap.MAINTAIN_ASPECT_RATIO;
import static org.alfresco.transform.common.RequestParamMap.RESIZE_HEIGHT;
import static org.alfresco.transform.common.RequestParamMap.RESIZE_PERCENTAGE;
import static org.alfresco.transform.common.RequestParamMap.RESIZE_WIDTH;
import static org.alfresco.transform.common.RequestParamMap.START_PAGE;
import static org.alfresco.transform.common.RequestParamMap.THUMBNAIL;
import static org.alfresco.transform.common.RequestParamMap.TIMEOUT;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.alfresco.transform.base.TransformManager;
import org.alfresco.transform.base.executors.AbstractCommandExecutor;
import org.alfresco.transform.base.executors.RuntimeExec;
import org.alfresco.transform.base.util.CustomTransformerFileAdaptor;
import org.alfresco.transform.exceptions.TransformException;
import org.alfresco.transform.imagemagick.ImageMagickOptionsBuilder;
import org.alfresco.transform.imagemagick.transformers.page.PageRangeFactory;
/**
* Converts image files into different types of images. Transformer supports multi-page images and allows to specify via parameters `startPage` and `endPage` range of pages that should be converted. In case of a one-page target image type (like `jpeg` or `png`) parameters `startPage` and `endPage` will be set to 0 by default - this means that only first page will be converted.
*/
@Component
public class ImageMagickTransformer extends AbstractCommandExecutor implements CustomTransformerFileAdaptor
public class ImageMagickTransformer implements CustomTransformerFileAdaptor
{
private final List<String> singlePageFormats = List.of(MIMETYPE_IMAGE_BMP, MIMETYPE_IMAGE_JP2, MIMETYPE_IMAGE_JPEG, MIMETYPE_IMAGE_PNG, MIMETYPE_IMAGE_XWD);
private final ImageMagickCommandExecutor imageMagickCommandExecutor;
private final PageRangeFactory pageRangeFactory;
private final ImageMagickCommandOptions imageMagickCommandOptions;
@Value("${transform.core.imagemagick.exe}")
private String exe;
@Value("${transform.core.imagemagick.dyn}")
private String dyn;
@Value("${transform.core.imagemagick.root}")
private String root;
// Not currently used, but may be again in the future if we need an ImageMagick extension
@Value("${transform.core.imagemagick.coders}")
private String coders;
@Value("${transform.core.imagemagick.config}")
private String config;
@PostConstruct
private void createCommands()
public ImageMagickTransformer(ImageMagickCommandExecutor imageMagickCommandExecutor, PageRangeFactory pageRangeFactory, ImageMagickCommandOptions imageMagickCommandOptions)
{
if (exe == null || exe.isEmpty())
{
throw new IllegalArgumentException("ImageMagickTransformer IMAGEMAGICK_EXE variable cannot be null or empty");
}
if (dyn == null || dyn.isEmpty())
{
throw new IllegalArgumentException("ImageMagickTransformer IMAGEMAGICK_DYN variable cannot be null or empty");
}
if (root == null || root.isEmpty())
{
throw new IllegalArgumentException("ImageMagickTransformer IMAGEMAGICK_ROOT variable cannot be null or empty");
}
super.transformCommand = createTransformCommand();
super.checkCommand = createCheckCommand();
this.imageMagickCommandExecutor = imageMagickCommandExecutor;
this.pageRangeFactory = pageRangeFactory;
this.imageMagickCommandOptions = imageMagickCommandOptions;
}
@Override
@@ -116,73 +78,12 @@ public class ImageMagickTransformer extends AbstractCommandExecutor implements C
return "imagemagick";
}
@Override
protected RuntimeExec createTransformCommand()
{
RuntimeExec runtimeExec = new RuntimeExec();
Map<String, String[]> commandsAndArguments = new HashMap<>();
commandsAndArguments.put(".*",
new String[]{exe, "${source}", "SPLIT:${options}", "-strip", "-quiet", "${target}"});
runtimeExec.setCommandsAndArguments(commandsAndArguments);
Map<String, String> processProperties = new HashMap<>();
processProperties.put("MAGICK_HOME", root);
processProperties.put("DYLD_FALLBACK_LIBRARY_PATH", dyn);
processProperties.put("LD_LIBRARY_PATH", dyn);
// Optional properties (see also https://imagemagick.org/script/resources.php#environment)
if (coders != null && !coders.isBlank())
{
processProperties.put("MAGICK_CODER_MODULE_PATH", coders);
}
if (config != null && !config.isBlank())
{
processProperties.put("MAGICK_CONFIGURE_PATH", config);
}
runtimeExec.setProcessProperties(processProperties);
Map<String, String> defaultProperties = new HashMap<>();
defaultProperties.put("options", null);
runtimeExec.setDefaultProperties(defaultProperties);
runtimeExec.setErrorCodes(
"1,2,255,400,405,410,415,420,425,430,435,440,450,455,460,465,470,475,480,485,490,495,499,700,705,710,715,720,725,730,735,740,750,755,760,765,770,775,780,785,790,795,799");
return runtimeExec;
}
@Override
protected RuntimeExec createCheckCommand()
{
RuntimeExec runtimeExec = new RuntimeExec();
Map<String, String[]> commandsAndArguments = new HashMap<>();
commandsAndArguments.put(".*", new String[]{exe, "-version"});
runtimeExec.setCommandsAndArguments(commandsAndArguments);
return runtimeExec;
}
@Override
public void transform(String sourceMimetype, String targetMimetype, Map<String, String> transformOptions,
File sourceFile, File targetFile, TransformManager transformManager) throws TransformException
{
String startPageString = transformOptions.get(START_PAGE);
String endPageString = transformOptions.get(END_PAGE);
if (!singlePageFormats.contains(sourceMimetype) && singlePageFormats.contains(targetMimetype))
{
if (StringUtils.isEmpty(startPageString))
{
startPageString = "0";
}
if (StringUtils.isEmpty(endPageString))
{
endPageString = startPageString;
}
}
final String options = ImageMagickOptionsBuilder
ImageMagickOptionsBuilder builder = ImageMagickOptionsBuilder
.builder()
.withStartPage(startPageString)
.withEndPage(endPageString)
.withAlphaRemove(transformOptions.get(ALPHA_REMOVE))
.withAutoOrient(transformOptions.get(AUTO_ORIENT))
.withCropGravity(transformOptions.get(CROP_GRAVITY))
@@ -196,27 +97,17 @@ public class ImageMagickTransformer extends AbstractCommandExecutor implements C
.withResizeHeight(transformOptions.get(RESIZE_HEIGHT))
.withResizePercentage(transformOptions.get(RESIZE_PERCENTAGE))
.withAllowEnlargement(transformOptions.get(ALLOW_ENLARGEMENT))
.withMaintainAspectRatio(transformOptions.get(MAINTAIN_ASPECT_RATIO))
.withCommandOptions(transformOptions.get(COMMAND_OPTIONS))
.build();
.withMaintainAspectRatio(transformOptions.get(MAINTAIN_ASPECT_RATIO));
String pageRange = calculatePageRange(
stringToInteger(startPageString),
stringToInteger(endPageString));
if (imageMagickCommandOptions.isCommandOptionsEnabled())
{
builder.withCommandOptions(transformOptions.get(COMMAND_OPTIONS));
}
String options = builder.build();
String pageRange = pageRangeFactory.create(sourceMimetype, targetMimetype, transformOptions);
Long timeout = stringToLong(transformOptions.get(TIMEOUT));
run(options, sourceFile, pageRange, targetFile, timeout);
}
private static String calculatePageRange(Integer startPage, Integer endPage)
{
return startPage == null
? endPage == null
? ""
: "[" + endPage + ']'
: endPage == null || startPage.equals(endPage)
? "[" + startPage + ']'
: "[" + startPage + '-' + endPage + ']';
imageMagickCommandExecutor.run(options, sourceFile, pageRange, targetFile, timeout);
}
}
@@ -0,0 +1,79 @@
/*
* #%L
* Alfresco Transform Core
* %%
* Copyright (C) 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.transform.imagemagick.transformers.page;
import static org.alfresco.transform.base.util.Util.stringToInteger;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_BMP;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JP2;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_PNG;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_XWD;
import static org.alfresco.transform.common.RequestParamMap.END_PAGE;
import static org.alfresco.transform.common.RequestParamMap.START_PAGE;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@Component
public class PageRangeFactory
{
private final List<String> singlePageFormats = List.of(MIMETYPE_IMAGE_BMP, MIMETYPE_IMAGE_JP2, MIMETYPE_IMAGE_JPEG, MIMETYPE_IMAGE_PNG, MIMETYPE_IMAGE_XWD);
public String create(String sourceMimetype, String targetMimetype, Map<String, String> transformOptions)
{
String startPageString = transformOptions.get(START_PAGE);
String endPageString = transformOptions.get(END_PAGE);
if (!singlePageFormats.contains(sourceMimetype) && singlePageFormats.contains(targetMimetype))
{
if (StringUtils.isEmpty(startPageString))
{
startPageString = "0";
}
if (StringUtils.isEmpty(endPageString))
{
endPageString = startPageString;
}
}
Integer startPage = stringToInteger(startPageString);
Integer endPage = stringToInteger(endPageString);
return calculatePageRange(startPage, endPage);
}
private String calculatePageRange(Integer startPage, Integer endPage)
{
return startPage == null
? endPage == null
? ""
: "[" + endPage + ']'
: endPage == null || startPage.equals(endPage)
? "[" + startPage + ']'
: "[" + startPage + '-' + endPage + ']';
}
}
@@ -3,8 +3,10 @@ queue:
transform:
core:
imagemagick:
root: ${IMAGEMAGICK_ROOT:/usr/lib64/ImageMagick-7.0.10}
dyn: ${IMAGEMAGICK_DYN:/usr/lib64/ImageMagick-7.0.10/lib}
root: ${IMAGEMAGICK_ROOT:/usr/lib64/ImageMagick-7.1.2}
dyn: ${IMAGEMAGICK_DYN:/usr/lib64/ImageMagick-7.1.2/lib}
exe: ${IMAGEMAGICK_EXE:/usr/bin/convert}
coders: ${IMAGEMAGICK_CODERS:}
config: ${IMAGEMAGICK_CONFIG:}
commandOptions:
enabled: ${IMAGEMAGICK_COMMAND_OPTIONS_ENABLED:false}
@@ -1114,4 +1114,4 @@
]
}
]
}
}
@@ -2,7 +2,7 @@
* #%L
* Alfresco Transform Core
* %%
* Copyright (C) 2005 - 2022 Alfresco Software Limited
* Copyright (C) 2005 - 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
@@ -79,7 +79,7 @@ import org.alfresco.transform.base.model.FileRefEntity;
import org.alfresco.transform.base.model.FileRefResponse;
import org.alfresco.transform.client.model.TransformReply;
import org.alfresco.transform.client.model.TransformRequest;
import org.alfresco.transform.imagemagick.transformers.ImageMagickTransformer;
import org.alfresco.transform.imagemagick.transformers.ImageMagickCommandExecutor;
/**
* Test ImageMagick with mocked external command.
@@ -89,7 +89,7 @@ public class ImageMagickTest extends AbstractBaseTest
private static String PREFIX_IMAGE = "image/";
@Autowired
private ImageMagickTransformer imageMagickTransformer;
private ImageMagickCommandExecutor imageMagickCommandExecutor;
@Mock
protected ExecutionResult mockExecutionResult;
@@ -111,7 +111,7 @@ public class ImageMagickTest extends AbstractBaseTest
@BeforeEach
public void before() throws IOException
{
setMockExternalCommandsOnTransformer(imageMagickTransformer, mockTransformCommand, mockCheckCommand);
setMockExternalCommandsOnTransformer(imageMagickCommandExecutor, mockTransformCommand, mockCheckCommand);
mockTransformCommand("jpg", "png", "image/jpeg", true);
}
@@ -312,10 +312,9 @@ public class ImageMagickTest extends AbstractBaseTest
}
@Test
public void deprecatedCommandOptionsTest() throws Exception
public void deprecatedCommandOptionsIsSkippedByDefaultTest() throws Exception
{
// Example of why the commandOptions parameter is a bad idea.
expectedOptions = "( horrible command / ); -auto-orient -resize 321x654";
expectedOptions = "-auto-orient -resize 321x654";
mockMvc
.perform(MockMvcRequestBuilders
.multipart(ENDPOINT_TRANSFORM)
@@ -0,0 +1,86 @@
/*
* #%L
* Alfresco Transform Core
* %%
* Copyright (C) 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.transform.imagemagick;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.alfresco.transform.imagemagick.transformers.ImageMagickCommandExecutor;
import org.alfresco.transform.imagemagick.transformers.ImageMagickCommandOptions;
import org.alfresco.transform.imagemagick.transformers.ImageMagickTransformer;
import org.alfresco.transform.imagemagick.transformers.page.PageRangeFactory;
public class ImageMagickTransformerTest
{
private ImageMagickCommandExecutor imageMagickCommandExecutor;
private ImageMagickCommandOptions imageMagickCommandOptions;
private ImageMagickTransformer imageMagickTransformer;
@BeforeEach
void setUp()
{
imageMagickCommandExecutor = mock(ImageMagickCommandExecutor.class);
PageRangeFactory pageRangeFactory = mock(PageRangeFactory.class);
imageMagickCommandOptions = mock(ImageMagickCommandOptions.class);
imageMagickTransformer = new ImageMagickTransformer(imageMagickCommandExecutor, pageRangeFactory, imageMagickCommandOptions);
}
@Test
void shouldNotAllowArgumentInjectionThroughCommandOptionsWhenDisabled()
{
when(imageMagickCommandOptions.isCommandOptionsEnabled()).thenReturn(false);
Map<String, String> transformOptions = Map.of("commandOptions", "( horrible command / );");
imageMagickTransformer.transform(null, null, transformOptions, null, null, null);
ArgumentCaptor<String> optionsCaptor = ArgumentCaptor.forClass(String.class);
verify(imageMagickCommandExecutor).run(optionsCaptor.capture(), any(), any(), any(), any());
assertThat(optionsCaptor.getValue()).doesNotContain("horrible", "horrible command", "( horrible command / );");
}
@Test
void shouldAllowArgumentInjectionThroughCommandOptionsWhenEnabled()
{
when(imageMagickCommandOptions.isCommandOptionsEnabled()).thenReturn(true);
Map<String, String> transformOptions = Map.of("commandOptions", "( horrible command / );");
imageMagickTransformer.transform(null, null, transformOptions, null, null, null);
ArgumentCaptor<String> optionsCaptor = ArgumentCaptor.forClass(String.class);
verify(imageMagickCommandExecutor).run(optionsCaptor.capture(), any(), any(), any(), any());
assertThat(optionsCaptor.getValue()).contains("horrible", "horrible command", "( horrible command / );");
}
}
@@ -0,0 +1,46 @@
/*
* #%L
* Alfresco Transform Core
* %%
* Copyright (C) 2025 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
* -
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* -
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
* -
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.transform.imagemagick.transformers;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = {ImageMagickCommandOptions.class})
class ImageMagickCommandOptionsTest
{
@Value("${transform.core.imagemagick.commandOptions.enabled}")
boolean commandOptionsEnabled;
@Test
void shouldBeDisabledByDefault()
{
assertFalse(commandOptionsEnabled);
}
}