This is an automated email from the ASF dual-hosted git repository.
mridulpathak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new 374fdbae25 Improved: Reject uploaded PDF files with hidden attachments
or active actions (OFBIZ-13693)
374fdbae25 is described below
commit 374fdbae251cea5028d586ad8c34d13dbb580612
Author: Mridul Pathak <[email protected]>
AuthorDate: Sun Oct 18 11:00:00 2026 +0530
Improved: Reject uploaded PDF files with hidden attachments or active
actions (OFBIZ-13693)
- SecuredUpload.isValidPdfFile only looked for embedded files in the
catalog's embedded files names tree, so a file attached through a file
attachment annotation, or through any other file specification with an embedded
file, was accepted. The check now walks every PDF object, like the JavaScript
check, and rejects any embedded file except the single ZUGFeRD invoice of the
names tree.
- The same walk rejects Launch, ImportData, GoToR, GoToE, Movie, Sound,
Rendition and RichMediaExecute actions, RichMedia, Movie, Sound and Screen
annotations, links that are not http, https or mailto, and form submissions
that are not http or https.
- With the default allowZUGFeRDnotSecure=false, a PDF with a single valid
ZUGFeRD invoice was always rejected, because the XML check compared
document.toString() with "[#document: null]", which is what every parsed DOM
document returns. It now rejects only when no XML document can be read.
---
.../org/apache/ofbiz/security/SecuredUpload.java | 120 ++++++++++++++++++++-
1 file changed, 117 insertions(+), 3 deletions(-)
diff --git
a/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
b/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
index 0a210479d4..91112ad74c 100644
---
a/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
+++
b/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
@@ -94,11 +94,13 @@ import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSDocument;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
+import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
import org.apache.pdfbox.pdmodel.PDDocument;
//import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
+import
org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.tika.Tika;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
@@ -1110,6 +1112,106 @@ public class SecuredUpload {
return exemptFieldScripts;
}
+ /** Actions that start a program, import data from a file, open another or
an embedded document, or play media. */
+ private static final Set<COSName> UNSAFE_ACTIONS =
Set.of(COSName.getPDFName("Launch"), COSName.getPDFName("ImportData"),
+ COSName.getPDFName("GoToR"), COSName.getPDFName("GoToE"),
COSName.getPDFName("Movie"), COSName.getPDFName("Sound"),
+ COSName.getPDFName("Rendition"),
COSName.getPDFName("RichMediaExecute"));
+
+ /** Annotations that play media. */
+ private static final Set<COSName> UNSAFE_ANNOTATIONS =
Set.of(COSName.getPDFName("RichMedia"), COSName.getPDFName("Movie"),
+ COSName.getPDFName("Sound"), COSName.getPDFName("Screen"));
+
+ private static final COSName ACTION = COSName.getPDFName("Action");
+ private static final COSName SUBMIT_FORM =
COSName.getPDFName("SubmitForm");
+ private static final Set<String> URI_SCHEMES = Set.of("http", "https",
"mailto");
+ private static final Set<String> SUBMIT_FORM_SCHEMES = Set.of("http",
"https");
+
+ /**
+ * Looks for content a PDF viewer could use to reach outside the document:
a file specification with an embedded file
+ * (in the embedded files names tree, a file attachment annotation, or
anywhere else), an action that starts a
+ * program, imports data from a file, opens another or an embedded
document or plays media, a media annotation, a link
+ * (URI action) that is not http, https or mailto, and a form submission
to anything other than http or https. Every
+ * object reachable from the trailer is checked, then every object listed
in the cross-reference table, even if nothing
+ * refers to it.
+ * @param pdDocument the loaded PDF document
+ * @param allowedFileSpec the file specification whose embedded file is
allowed, null if none is
+ * @return what was found, or null if nothing was
+ */
+ private static String findUnsafeContent(PDDocument pdDocument, COSBase
allowedFileSpec) {
+ COSDocument cosDocument = pdDocument.getDocument();
+ Deque<COSBase> toVisit = new ArrayDeque<>();
+ cosDocument.getXrefTable().keySet().forEach(key ->
pushIfNotNull(toVisit, cosDocument.getObjectFromPool(key)));
+ pushIfNotNull(toVisit, cosDocument.getTrailer());
+ Set<COSBase> visited = Collections.newSetFromMap(new
IdentityHashMap<>());
+ while (!toVisit.isEmpty()) {
+ COSBase base = dereference(toVisit.pop());
+ if (base == null || !visited.add(base)) {
+ continue;
+ }
+ if (base instanceof COSDictionary) {
+ COSDictionary dictionary = (COSDictionary) base;
+ String unsafeContent = getUnsafeContent(dictionary,
allowedFileSpec);
+ if (unsafeContent != null) {
+ return unsafeContent;
+ }
+ dictionary.getValues().forEach(value -> pushIfNotNull(toVisit,
value));
+ } else if (base instanceof COSArray) {
+ COSArray array = (COSArray) base;
+ array.forEach(element -> pushIfNotNull(toVisit, element));
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return what makes the given PDF dictionary unsafe, or null if it is not
+ * @see #findUnsafeContent(PDDocument, COSBase)
+ */
+ private static String getUnsafeContent(COSDictionary dictionary, COSBase
allowedFileSpec) {
+ if (dictionary.containsKey(COSName.EF) && dictionary !=
allowedFileSpec) {
+ return "an embedded file";
+ }
+ COSName type = dictionary.getCOSName(COSName.TYPE);
+ COSName actionType = dictionary.getCOSName(COSName.S);
+ if (actionType != null && (type == null || ACTION.equals(type))) {
+ if (UNSAFE_ACTIONS.contains(actionType)) {
+ return "a " + actionType.getName() + " action";
+ }
+ if (COSName.URI.equals(actionType) &&
!hasScheme(dictionary.getString(COSName.URI), URI_SCHEMES)) {
+ return "a link that is not http, https or mailto";
+ }
+ if (SUBMIT_FORM.equals(actionType) &&
!hasScheme(getFileSpecName(dictionary.getDictionaryObject(COSName.F)),
+ SUBMIT_FORM_SCHEMES)) {
+ return "a form submission that is not http or https";
+ }
+ }
+ COSName subtype = dictionary.getCOSName(COSName.SUBTYPE);
+ if (subtype != null && (type == null || COSName.ANNOT.equals(type)) &&
UNSAFE_ANNOTATIONS.contains(subtype)) {
+ return "a " + subtype.getName() + " annotation";
+ }
+ return null;
+ }
+
+ /** @return the file name or URL of a PDF file specification, either a
string or a dictionary, or null if it has none */
+ private static String getFileSpecName(COSBase fileSpec) {
+ if (fileSpec instanceof COSDictionary) {
+ COSDictionary dictionary = (COSDictionary) fileSpec;
+ String unicodeName = dictionary.getString(COSName.UF);
+ return unicodeName != null ? unicodeName :
dictionary.getString(COSName.F);
+ }
+ return fileSpec instanceof COSString ? ((COSString)
fileSpec).getString() : null;
+ }
+
+ /** @return true if the target is an absolute URL using one of the given
lower case schemes */
+ private static boolean hasScheme(String target, Set<String> schemes) {
+ if (target == null) {
+ return false;
+ }
+ String trimmedTarget = target.trim();
+ int colon = trimmedTarget.indexOf(':');
+ return colon > 0 && schemes.contains(trimmedTarget.substring(0,
colon).toLowerCase(Locale.ROOT));
+ }
+
private static COSBase dereference(COSBase base) {
return base instanceof COSObject ? ((COSObject) base).getObject() :
base;
}
@@ -1137,6 +1239,7 @@ public class SecuredUpload {
}
// Load stream in PDF parser
PDEmbeddedFilesNameTreeNode efTree = null;
+ boolean zUGFeRDCompliantUploadAllowed =
UtilProperties.getPropertyAsBoolean("security", "allowZUGFeRDCompliantUpload",
false);
try (PDDocument pdDocument = Loader.loadPDF(new
RandomAccessReadBufferedFile(fileName))) {
// Check 1: detect if the document contains any JavaScript code
boolean formFieldScriptsAllowed =
UtilProperties.getPropertyAsBoolean("security", "allowPdfFormFieldScripts",
false);
@@ -1145,11 +1248,22 @@ public class SecuredUpload {
+ "For security reason it's not accepted as a PDF
file", MODULE);
return safeState;
}
- // OK no JS code, pass to check 2: detect if the document has
any embedded files
+ // OK no JS code, pass to check 2: detect if the document has
any embedded files, other than a ZUGFeRD invoice as the
+ // only file of the embedded files names tree, or any other
unsafe content
PDDocumentNameDictionary names = new
PDDocumentNameDictionary(pdDocument.getDocumentCatalog());
efTree = names.getEmbeddedFiles();
+ Map<String, PDComplexFileSpecification> embeddedFiles =
Objects.isNull(efTree) ? null : efTree.getNames();
+ COSBase zUGFeRDFileSpec = null;
+ if (zUGFeRDCompliantUploadAllowed && embeddedFiles != null &&
embeddedFiles.size() == 1) {
+ zUGFeRDFileSpec =
embeddedFiles.values().iterator().next().getCOSObject();
+ }
+ String unsafeContent = findUnsafeContent(pdDocument,
zUGFeRDFileSpec);
+ if (unsafeContent != null) {
+ Debug.logInfo("The file " + file.getAbsolutePath() + "
contains " + unsafeContent + ". "
+ + "For security reason it's not accepted as a PDF
file", MODULE);
+ return safeState;
+ }
}
- boolean zUGFeRDCompliantUploadAllowed =
UtilProperties.getPropertyAsBoolean("security", "allowZUGFeRDCompliantUpload",
false);
if (zUGFeRDCompliantUploadAllowed && !Objects.isNull(efTree)) {
canParseZUGFeRD = false;
Integer numberOfEmbeddedFiles = efTree.getNames().size();
@@ -1161,7 +1275,7 @@ public class SecuredUpload {
} else {
try {
Document document =
UtilXml.readXmlDocument(importer.getUTF8());
- if (document.toString().equals("[#document:
null]")) {
+ if (document == null ||
document.getDocumentElement() == null) {
safeState = false;
Debug.logInfo("The file " +
file.getAbsolutePath()
+ " is not a readable (valid and
secure) PDF file. For security reason it's not accepted as a such file",