Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ public class PSTemplateInfo extends HttpServlet {

private static final int DEFAULT_BUFFER_SIZE = 20480; // 20KB.

// T2.6 hardening (issue #82): bound the multipart upload to prevent DoS via huge
// payloads against commons-fileupload 1.6.0 (which has 7 CVEs in that area).
// Values match the existing PSAssetUploadServlet config in WebUI/war/WEB-INF/web.xml
// (100MB file / 400MB request) but tuned for template XML files which are smaller.
private static final int UPLOAD_MEMORY_THRESHOLD = 1 << 20; // 1MB
private static final long UPLOAD_MAX_FILE_SIZE = 50L << 20; // 50MB
private static final long UPLOAD_MAX_REQUEST_SIZE = 100L << 20; // 100MB

private static final Logger log = LogManager.getLogger(PSTemplateInfo.class);

public PSTemplateInfo() {
Expand Down Expand Up @@ -121,8 +129,15 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)

if (isMultipart) {
try {
List<FileItem> items =
new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request);
// T2.6 hardening: configure commons-fileupload with explicit size limits.
// The previous `new ServletFileUpload(new DiskFileItemFactory())` had no
// bounds, leaving the server open to DoS via arbitrarily large uploads.
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(UPLOAD_MEMORY_THRESHOLD);
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(UPLOAD_MAX_FILE_SIZE);
upload.setSizeMax(UPLOAD_MAX_REQUEST_SIZE);
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
templateImported = importTemplate(siteId, item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ public class PSTemplateServlet extends HttpServlet {

private static final int DEFAULT_BUFFER_SIZE = 20480; // 20KB.

// T2.6 hardening (issue #82): bound the multipart upload to prevent DoS via huge
// payloads against commons-fileupload 1.6.0 (which has 7 CVEs in that area).
// Values match the existing PSAssetUploadServlet config in WebUI/war/WEB-INF/web.xml
// (100MB file / 400MB request) but tuned for template XML files which are smaller.
private static final int UPLOAD_MEMORY_THRESHOLD = 1 << 20; // 1MB
private static final long UPLOAD_MAX_FILE_SIZE = 50L << 20; // 50MB
private static final long UPLOAD_MAX_REQUEST_SIZE = 100L << 20; // 100MB

public PSTemplateServlet() {
PSSpringWebApplicationContextUtils.injectDependencies(this);
}
Expand Down Expand Up @@ -113,8 +121,15 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)

if (isMultipart) {
try {
List<FileItem> items =
new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request);
// T2.6 hardening: configure commons-fileupload with explicit size limits.
// The previous `new ServletFileUpload(new DiskFileItemFactory())` had no
// bounds, leaving the server open to DoS via arbitrarily large uploads.
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(UPLOAD_MEMORY_THRESHOLD);
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(UPLOAD_MAX_FILE_SIZE);
upload.setSizeMax(UPLOAD_MAX_REQUEST_SIZE);
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
templateImported = importTemplate(siteId, item);
Expand Down
77 changes: 77 additions & 0 deletions system/src/main/java/com/percussion/util/PSArchiveFiles.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,45 @@ public class PSArchiveFiles {

private static final Logger log = LogManager.getLogger(IPSConstants.PACKAGING_LOG);

// T2.6 hardening (issue #82): bound archive extraction to limit exposure to the
// 11 CVEs in commons-compress 1.28.0 (mostly zip-bomb / oversized-entry attacks).
// The path-traversal half is already covered by the ZipSlipGuard + canonical-path
// check below. These three limits cover the resource-exhaustion half.
// Defaults are tuned to "huge but realistic" (a 100MB war, 10K entries, 500MB total
// uncompressed); operators can override per-call by setting the system properties
// PSARCHIVE_MAX_ENTRY_SIZE / PSARCHIVE_MAX_ENTRIES / PSARCHIVE_MAX_TOTAL_SIZE.
private static final long DEFAULT_MAX_ENTRY_SIZE = 100L << 20; // 100MB per entry
private static final int DEFAULT_MAX_ENTRIES = 10_000; // 10K entries per archive
private static final long DEFAULT_MAX_TOTAL_SIZE = 500L << 20; // 500MB total uncompressed

private static long getMaxEntrySize() {
return readLongProp("PSARCHIVE_MAX_ENTRY_SIZE", DEFAULT_MAX_ENTRY_SIZE);
}

private static int getMaxEntries() {
return (int) readLongProp("PSARCHIVE_MAX_ENTRIES", DEFAULT_MAX_ENTRIES);
}

private static long getMaxTotalSize() {
return readLongProp("PSARCHIVE_MAX_TOTAL_SIZE", DEFAULT_MAX_TOTAL_SIZE);
}

private static long readLongProp(String name, long def) {
String v = System.getProperty(name);
if (v == null || v.isEmpty()) return def;
try {
return Long.parseLong(v.trim());
} catch (NumberFormatException nfe) {
log.warn(
"Ignoring non-numeric system property {}={} ({}); using default {}",
name,
v,
nfe.getMessage(),
def);
return def;
}
}

/**
* Opens the specified archive file. The classes calling this method are responsible for closing
* this file.
Expand Down Expand Up @@ -344,10 +383,48 @@ public static String extractFilesFromArchive(
pw.println("Extracting files from archive");
}

// T2.6 hardening: read the size caps once per extraction so an attacker cannot
// cause repeated property lookups, and so the same limit is applied to every
// entry in this archive.
final long maxEntrySize = getMaxEntrySize();
final int maxEntries = getMaxEntries();
final long maxTotalSize = getMaxTotalSize();
int entriesSeen = 0;
long bytesSeen = 0L;

for (Enumeration files = archiveFile.entries(); files.hasMoreElements(); ) {
ZipEntry entry = (ZipEntry) files.nextElement();
StringBuilder errorBuf = new StringBuilder();

// T2.6 hardening: enforce MAX_ENTRIES and per-entry / total uncompressed size
// caps before doing any directory or file work. These limits are what protect
// against the zip-bomb half of the commons-compress 1.28.0 CVE set.
if (++entriesSeen > maxEntries) {
throw new SecurityException(
"Archive rejected: too many entries (limit=" + maxEntries + ")");
}
final long entrySize = entry.getSize();
if (entrySize > maxEntrySize) {
throw new SecurityException(
"Archive rejected: entry '"
+ entry.getName()
+ "' uncompressed size "
+ entrySize
+ " exceeds limit "
+ maxEntrySize);
}
if (entrySize > 0) {
bytesSeen += entrySize;
if (bytesSeen > maxTotalSize) {
throw new SecurityException(
"Archive rejected: total uncompressed size exceeds limit "
+ maxTotalSize
+ " (entry='"
+ entry.getName()
+ "')");
}
}

// Check whether the directory exists for this file. If not, create it.
String dir = "";
String name = entry.getName();
Expand Down
Loading