You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2017/03/20 07:05:20 UTC

[05/13] struts-extras git commit: Creates a plugin to allow mitigate vulnerability with S2-045 in older versions of Struts when using jakarta-stream Multipart parser

Creates a plugin to allow mitigate vulnerability with S2-045 in older versions of Struts when using jakarta-stream Multipart parser


Project: http://git-wip-us.apache.org/repos/asf/struts-extras/repo
Commit: http://git-wip-us.apache.org/repos/asf/struts-extras/commit/1cd42a60
Tree: http://git-wip-us.apache.org/repos/asf/struts-extras/tree/1cd42a60
Diff: http://git-wip-us.apache.org/repos/asf/struts-extras/diff/1cd42a60

Branch: refs/heads/master
Commit: 1cd42a6080dac524bd572e79f0c89b018b0c01a8
Parents: bd18c11
Author: Lukasz Lenart <lu...@gmail.com>
Authored: Sat Mar 18 14:54:34 2017 +0100
Committer: Lukasz Lenart <lu...@gmail.com>
Committed: Sat Mar 18 14:54:34 2017 +0100

----------------------------------------------------------------------
 .../pom.xml                                     |  75 +++
 .../SecureJakartaStreamMultiPartRequest.java    | 617 +++++++++++++++++++
 .../src/main/resources/struts-plugin.xml        |  33 +
 3 files changed, 725 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/struts-extras/blob/1cd42a60/struts2-secure-jakarta-stream-multipart-parser-plugin/pom.xml
----------------------------------------------------------------------
diff --git a/struts2-secure-jakarta-stream-multipart-parser-plugin/pom.xml b/struts2-secure-jakarta-stream-multipart-parser-plugin/pom.xml
new file mode 100644
index 0000000..f9fcada
--- /dev/null
+++ b/struts2-secure-jakarta-stream-multipart-parser-plugin/pom.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <parent>
+        <groupId>org.apache.struts</groupId>
+        <artifactId>struts-master</artifactId>
+        <version>10</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>struts2-secure-jakarta-stream-multipart-parser-plugin</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <name>Struts 2.3.20 - 2.5.5 secure Jakarta stream Multipart parser plugin</name>
+
+    <description>
+        This plugin allows to fix a vulnerability S2-045 without a need to migrate to the latest Struts versions
+    </description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.apache.struts</groupId>
+            <artifactId>struts2-core</artifactId>
+            <version>2.3.20</version>
+            <optional>true</optional>
+        </dependency>
+        
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.4</version>
+            <scope>provided</scope>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.6</source>
+                    <target>1.6</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

http://git-wip-us.apache.org/repos/asf/struts-extras/blob/1cd42a60/struts2-secure-jakarta-stream-multipart-parser-plugin/src/main/java/org/apache/struts/extras/SecureJakartaStreamMultiPartRequest.java
----------------------------------------------------------------------
diff --git a/struts2-secure-jakarta-stream-multipart-parser-plugin/src/main/java/org/apache/struts/extras/SecureJakartaStreamMultiPartRequest.java b/struts2-secure-jakarta-stream-multipart-parser-plugin/src/main/java/org/apache/struts/extras/SecureJakartaStreamMultiPartRequest.java
new file mode 100644
index 0000000..3acc55d
--- /dev/null
+++ b/struts2-secure-jakarta-stream-multipart-parser-plugin/src/main/java/org/apache/struts/extras/SecureJakartaStreamMultiPartRequest.java
@@ -0,0 +1,617 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts.extras;
+
+import com.opensymphony.xwork2.LocaleProvider;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.LocalizedTextUtil;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+import org.apache.commons.fileupload.FileItemIterator;
+import org.apache.commons.fileupload.FileItemStream;
+import org.apache.commons.fileupload.FileUploadBase;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.fileupload.util.Streams;
+import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class SecureJakartaStreamMultiPartRequest implements MultiPartRequest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SecureJakartaStreamMultiPartRequest.class);
+
+    /**
+     * Defines the internal buffer size used during streaming operations.
+     */
+    private static final int BUFFER_SIZE = 10240;
+
+    /**
+     * Map between file fields and file data.
+     */
+    private Map<String, List<FileInfo>> fileInfos = new HashMap<String, List<FileInfo>>();
+
+    /**
+     * Map between non-file fields and values.
+     */
+    private Map<String, List<String>> parameters = new HashMap<String, List<String>>();
+
+    /**
+     * Internal list of raised errors to be passed to the the Struts2 framework.
+     */
+    private List<String> errors = new ArrayList<String>();
+
+    /**
+     * Internal list of non-critical messages to be passed to the Struts2 framework.
+     */
+    private List<String> messages = new ArrayList<String>();
+
+    /**
+     * Specifies the maximum size of the entire request.
+     */
+    private Long maxSize;
+
+    /**
+     * Specifies the buffer size to use during streaming.
+     */
+    private int bufferSize = BUFFER_SIZE;
+
+    /**
+     * Localization to be used regarding errors.
+     */
+    private Locale defaultLocale = Locale.ENGLISH;
+
+    /**
+     * Injects the Struts multiple part maximum size.
+     *
+     * @param maxSize
+     */
+    @Inject(StrutsConstants.STRUTS_MULTIPART_MAXSIZE)
+    public void setMaxSize(String maxSize) {
+        this.maxSize = Long.parseLong(maxSize);
+    }
+
+    /**
+     * Sets the buffer size to be used.
+     *
+     * @param bufferSize
+     */
+    @Inject(value = StrutsConstants.STRUTS_MULTIPART_BUFFERSIZE, required = false)
+    public void setBufferSize(String bufferSize) {
+        this.bufferSize = Integer.parseInt(bufferSize);
+    }
+
+    /**
+     * Injects the Struts locale provider.
+     *
+     * @param provider
+     */
+    @Inject
+    public void setLocaleProvider(LocaleProvider provider) {
+        defaultLocale = provider.getLocale();
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
+     */
+    public void cleanUp() {
+        LOG.debug("Performing File Upload temporary storage cleanup.");
+        for (String fieldName : fileInfos.keySet()) {
+            for (FileInfo fileInfo : fileInfos.get(fieldName)) {
+                File file = fileInfo.getFile();
+                LOG.debug("Deleting file '#0'.", file.getName());
+                if (!file.delete())
+                    LOG.warn("There was a problem attempting to delete file '#0'.", file.getName());
+            }
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String)
+     */
+    public String[] getContentType(String fieldName) {
+        List<FileInfo> infos = fileInfos.get(fieldName);
+        if (infos == null)
+            return null;
+
+        List<String> types = new ArrayList<String>(infos.size());
+        for (FileInfo fileInfo : infos)
+            types.add(fileInfo.getContentType());
+
+        return types.toArray(new String[types.size()]);
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors()
+     */
+    public List<String> getErrors() {
+        return errors;
+    }
+
+    /**
+     * Allows interceptor to fetch non-critical messages that can be passed to the action.
+     *
+     * @return
+     */
+    public List<String> getMesssages() {
+        return messages;
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String)
+     */
+    public File[] getFile(String fieldName) {
+        List<FileInfo> infos = fileInfos.get(fieldName);
+        if (infos == null)
+            return null;
+
+        List<File> files = new ArrayList<File>(infos.size());
+        for (FileInfo fileInfo : infos)
+            files.add(fileInfo.getFile());
+
+        return files.toArray(new File[files.size()]);
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String)
+     */
+    public String[] getFileNames(String fieldName) {
+        List<FileInfo> infos = fileInfos.get(fieldName);
+        if (infos == null)
+            return null;
+
+        List<String> names = new ArrayList<String>(infos.size());
+        for (FileInfo fileInfo : infos)
+            names.add(getCanonicalName(fileInfo.getOriginalName()));
+
+        return names.toArray(new String[names.size()]);
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
+     */
+    public Enumeration<String> getFileParameterNames() {
+        return Collections.enumeration(fileInfos.keySet());
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String)
+     */
+    public String[] getFilesystemName(String fieldName) {
+        List<FileInfo> infos = fileInfos.get(fieldName);
+        if (infos == null)
+            return null;
+
+        List<String> names = new ArrayList<String>(infos.size());
+        for (FileInfo fileInfo : infos)
+            names.add(fileInfo.getFile().getName());
+
+        return names.toArray(new String[names.size()]);
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String)
+     */
+    public String getParameter(String name) {
+        List<String> values = parameters.get(name);
+        if (values != null && values.size() > 0)
+            return values.get(0);
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames()
+     */
+    public Enumeration<String> getParameterNames() {
+        return Collections.enumeration(parameters.keySet());
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String)
+     */
+    public String[] getParameterValues(String name) {
+        List<String> values = parameters.get(name);
+        if (values != null && values.size() > 0)
+            return values.toArray(new String[values.size()]);
+        return null;
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#parse(javax.servlet.http.HttpServletRequest, java.lang.String)
+     */
+    public void parse(HttpServletRequest request, String saveDir)
+            throws IOException {
+        try {
+            setLocale(request);
+            processUpload(request, saveDir);
+        } catch (Exception e) {
+            e.printStackTrace();
+            String errorMessage = buildErrorMessage(e, new Object[]{});
+            if (!errors.contains(errorMessage))
+                errors.add(errorMessage);
+        }
+    }
+
+    /**
+     * Inspect the servlet request and set the locale if one wasn't provided by
+     * the Struts2 framework.
+     *
+     * @param request
+     */
+    protected void setLocale(HttpServletRequest request) {
+        if (defaultLocale == null)
+            defaultLocale = request.getLocale();
+    }
+
+    /**
+     * Processes the upload.
+     *
+     * @param request
+     * @param saveDir
+     * @throws Exception
+     */
+    private void processUpload(HttpServletRequest request, String saveDir)
+            throws Exception {
+
+        // Sanity check that the request is a multi-part/form-data request.
+        if (ServletFileUpload.isMultipartContent(request)) {
+
+            // Sanity check on request size.
+            boolean requestSizePermitted = isRequestSizePermitted(request);
+
+            // Interface with Commons FileUpload API
+            // Using the Streaming API
+            ServletFileUpload servletFileUpload = new ServletFileUpload();
+            FileItemIterator i = servletFileUpload.getItemIterator(request);
+
+            // Iterate the file items
+            while (i.hasNext()) {
+                try {
+                    FileItemStream itemStream = i.next();
+
+                    // If the file item stream is a form field, delegate to the
+                    // field item stream handler
+                    if (itemStream.isFormField()) {
+                        processFileItemStreamAsFormField(itemStream);
+                    }
+
+                    // Delegate the file item stream for a file field to the
+                    // file item stream handler, but delegation is skipped
+                    // if the requestSizePermitted check failed based on the
+                    // complete content-size of the request.
+                    else {
+
+                        // prevent processing file field item if request size not allowed.
+                        // also warn user in the logs.
+                        if (!requestSizePermitted) {
+                            addFileSkippedError(itemStream.getName(), request);
+                            LOG.warn("Skipped stream '#0', request maximum size (#1) exceeded.", itemStream.getName(), maxSize);
+                            continue;
+                        }
+
+                        processFileItemStreamAsFileField(itemStream, saveDir);
+                    }
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    /**
+     * Defines whether the request allowed based on content length.
+     *
+     * @param request
+     * @return
+     */
+    private boolean isRequestSizePermitted(HttpServletRequest request) {
+        // if maxSize is specified as -1, there is no sanity check and it's
+        // safe to return true for any request, delegating the failure
+        // checks later in the upload process.
+        if (maxSize == -1 || request == null)
+            return true;
+
+        return request.getContentLength() < maxSize;
+    }
+
+    /**
+     * Get the request content length.
+     *
+     * @param request
+     * @return
+     */
+    private long getRequestSize(HttpServletRequest request) {
+        long requestSize = 0;
+        if (request != null)
+            requestSize = request.getContentLength();
+        return requestSize;
+    }
+
+    /**
+     * Add a file skipped message notification for action messages.
+     *
+     * @param fileName
+     * @param request
+     */
+    private void addFileSkippedError(String fileName, HttpServletRequest request) {
+        String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded.";
+        FileUploadBase.FileSizeLimitExceededException exception = new FileUploadBase.FileSizeLimitExceededException(exceptionMessage, getRequestSize(request), maxSize);
+        String message = buildMessage(exception, new Object[]{fileName, getRequestSize(request), maxSize});
+        if (!errors.contains(message))
+            errors.add(message);
+    }
+
+    /**
+     * Processes the FileItemStream as a Form Field.
+     *
+     * @param itemStream
+     */
+    private void processFileItemStreamAsFormField(FileItemStream itemStream) {
+        String fieldName = itemStream.getFieldName();
+        try {
+            List<String> values = null;
+            String fieldValue = Streams.asString(itemStream.openStream());
+            if (!parameters.containsKey(fieldName)) {
+                values = new ArrayList<String>();
+                parameters.put(fieldName, values);
+            } else {
+                values = parameters.get(fieldName);
+            }
+            values.add(fieldValue);
+        } catch (IOException e) {
+            e.printStackTrace();
+            LOG.warn("Failed to handle form field '#0'.", fieldName);
+        }
+    }
+
+    /**
+     * Processes the FileItemStream as a file field.
+     *
+     * @param itemStream
+     * @param location
+     */
+    private void processFileItemStreamAsFileField(FileItemStream itemStream, String location) {
+        File file = null;
+        try {
+            // Create the temporary upload file.
+            file = createTemporaryFile(itemStream.getName(), location);
+
+            if (streamFileToDisk(itemStream, file))
+                createFileInfoFromItemStream(itemStream, file);
+        } catch (IOException e) {
+            if (file != null) {
+                try {
+                    file.delete();
+                } catch (SecurityException se) {
+                    se.printStackTrace();
+                    LOG.warn("Failed to delete '#0' due to security exception above.", file.getName());
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates a temporary file based on the given filename and location.
+     *
+     * @param fileName
+     * @param location
+     * @return
+     * @throws IOException
+     */
+    private File createTemporaryFile(String fileName, String location)
+            throws IOException {
+        String name = fileName
+                .substring(fileName.lastIndexOf('/') + 1)
+                .substring(fileName.lastIndexOf('\\') + 1);
+
+        String prefix = name;
+        String suffix = "";
+
+        if (name.contains(".")) {
+            prefix = name.substring(0, name.lastIndexOf('.'));
+            suffix = name.substring(name.lastIndexOf('.'));
+        }
+
+        File file = File.createTempFile(prefix + "_", suffix, new File(location));
+        LOG.debug("Creating temporary file '#0' (originally '#1').", file.getName(), fileName);
+        return file;
+    }
+
+    /**
+     * Streams the file upload stream to the specified file.
+     *
+     * @param itemStream
+     * @param file
+     * @return
+     * @throws IOException
+     */
+    private boolean streamFileToDisk(FileItemStream itemStream, File file) throws IOException {
+        boolean result = false;
+        InputStream input = itemStream.openStream();
+        OutputStream output = null;
+        try {
+            output = new BufferedOutputStream(new FileOutputStream(file), bufferSize);
+            byte[] buffer = new byte[bufferSize];
+            LOG.debug("Streaming file using buffer size #0.", bufferSize);
+            for (int length = 0; ((length = input.read(buffer)) > 0); )
+                output.write(buffer, 0, length);
+            result = true;
+        } finally {
+            if (output != null) {
+                try {
+                    output.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (input != null) {
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Creates an internal <code>FileInfo</code> structure used to pass information
+     * to the <code>FileUploadInterceptor</code> during the interceptor stack
+     * invocation process.
+     *
+     * @param itemStream
+     * @param file
+     */
+    private void createFileInfoFromItemStream(FileItemStream itemStream, File file) {
+        // gather attributes from file upload stream.
+        String fileName = itemStream.getName();
+        String fieldName = itemStream.getFieldName();
+        // create internal structure
+        FileInfo fileInfo = new FileInfo(file, itemStream.getContentType(), fileName);
+        // append or create new entry.
+        if (!fileInfos.containsKey(fieldName)) {
+            List<FileInfo> infos = new ArrayList<FileInfo>();
+            infos.add(fileInfo);
+            fileInfos.put(fieldName, infos);
+        } else {
+            fileInfos.get(fieldName).add(fileInfo);
+        }
+    }
+
+    /**
+     * Get the canonical name based on the supplied filename.
+     *
+     * @param fileName
+     * @return
+     */
+    private String getCanonicalName(String fileName) {
+        int forwardSlash = fileName.lastIndexOf("/");
+        int backwardSlash = fileName.lastIndexOf("\\");
+        if (forwardSlash != -1 && forwardSlash > backwardSlash) {
+            fileName = fileName.substring(forwardSlash + 1, fileName.length());
+        } else {
+            fileName = fileName.substring(backwardSlash + 1, fileName.length());
+        }
+        return fileName;
+    }
+
+    /**
+     * Build error message.
+     *
+     * @param e
+     * @param args
+     * @return
+     */
+    private String buildErrorMessage(Throwable e, Object[] args) {
+        String errorKey = "struts.message.upload.error." + e.getClass().getSimpleName();
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Preparing error message for key: [#0]", errorKey);
+        }
+
+        if (LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, new Object[0]) == null) {
+            return LocalizedTextUtil.findText(this.getClass(), "struts.messages.error.uploading", defaultLocale, null, new Object[]{e.getMessage()});
+        } else {
+            return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, args);
+        }
+    }
+
+    /**
+     * Build action message.
+     *
+     * @param e
+     * @param args
+     * @return
+     */
+    private String buildMessage(Throwable e, Object[] args) {
+        String messageKey = "struts.message.upload.message." + e.getClass().getSimpleName();
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("Preparing message for key: [#0]", messageKey);
+        }
+
+        if (LocalizedTextUtil.findText(this.getClass(), messageKey, defaultLocale, null, new Object[0]) == null) {
+            return LocalizedTextUtil.findText(this.getClass(), "struts.messages.error.uploading", defaultLocale, null, new Object[]{e.getMessage()});
+        } else {
+            return LocalizedTextUtil.findText(this.getClass(), messageKey, defaultLocale, null, args);
+        }
+    }
+
+    /**
+     * Internal data structure used to store a reference to information needed
+     * to later pass post processing data to the <code>FileUploadInterceptor</code>.
+     *
+     * @version $Revision$
+     * @since 7.0.0
+     */
+    private static class FileInfo implements Serializable {
+
+        private File file;
+        private String contentType;
+        private String originalName;
+
+        /**
+         * Default constructor.
+         *
+         * @param file
+         * @param contentType
+         * @param originalName
+         */
+        public FileInfo(File file, String contentType, String originalName) {
+            this.file = file;
+            this.contentType = contentType;
+            this.originalName = originalName;
+        }
+
+        /**
+         * @return
+         */
+        public File getFile() {
+            return file;
+        }
+
+        /**
+         * @return
+         */
+        public String getContentType() {
+            return contentType;
+        }
+
+        /**
+         * @return
+         */
+        public String getOriginalName() {
+            return originalName;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts-extras/blob/1cd42a60/struts2-secure-jakarta-stream-multipart-parser-plugin/src/main/resources/struts-plugin.xml
----------------------------------------------------------------------
diff --git a/struts2-secure-jakarta-stream-multipart-parser-plugin/src/main/resources/struts-plugin.xml b/struts2-secure-jakarta-stream-multipart-parser-plugin/src/main/resources/struts-plugin.xml
new file mode 100644
index 0000000..de48c95
--- /dev/null
+++ b/struts2-secure-jakarta-stream-multipart-parser-plugin/src/main/resources/struts-plugin.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<!DOCTYPE struts PUBLIC
+    "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
+    "http://struts.apache.org/dtds/struts-2.3.dtd">
+
+<struts>
+
+  <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest"
+        class="org.apache.struts.extras.SecureJakartaStreamMultiPartRequest"
+        name="secure-jakarta-stream"
+        scope="prototype"/>
+
+  <constant name="struts.multipart.parser" value="secure-jakarta-stream"/>
+
+</struts>