Contrast Vulnerability found in 6.5 /content-api/content-service/content-items

We have started to use CONTRAST to find vulnerabilities in our code and we are getting the following vulnerability

Not sure if any subsequent releases after 6.5 have addressed this …

Below is information from Contrast concerning this issue

###############################################################################

Path Traversal from “processInstanceId” Parameter on “/content-api/content-service/content-items” page

What happened?

We tracked the following data from “processInstanceId” Parameter:

POST /content-api/content-service/content-items

processInstanceId=060b7192-cd7a-11ec-ab60-0a580aa80db7&createdBy=Doug Bogie&name=file1.txt&mimeType=text/plain

…which was accessed within the following code:

org.flowable.content.engine.impl.fs.SimpleFileSystemContentStorage#internalCreateOrGetFolder(), line 297

…and ended up being used as part of the path in the following file being opened:

060b7192-cd7a-11ec-ab60-0a580aa80db7

What’s the risk?

Because there is untrusted data being used as part of the file path, it may be possible for an attacker to read sensitive data or write, update, or delete arbitrary files on the container’s file system. The ability to write arbitrary files to the file system is also called Unrestricted or Arbitrary File Uploads.

Many file operations are intended to take place within a restricted directory. By using special elements such as “…” and “/” separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. This is called “path traversal”.

The application opens up a java.io.File or file I/O stream based on user input. Although it’s not directly clear how that file is being used, this functionality could be an avenue for path traversal abuse.

Here’s an example of a typical path traversal vulnerability:

String statement = request.getParameter(“statement”); if(!statement.endsWith(".xml")) { // Validate (weakly) this file is an xml file logger.error(“Bad filename sent”); return; } // Read the specified file File file = new File(STATEMENT_DIR, statement); FileInputStream fis = new FileInputStream(file); byte[] fileBytes = new byte[file.length()]; fis.read(fileBytes); response.getOutputStream().write(fileBytes);

Often, there is no filename validation at all. Either way, an attacker could abuse this functionality to view the /etc/passwd file on a UNIX system by passing the following value for the statement parameter:

http://yoursite.com/app/pathTraversal?statement=../../../../../../../../etc/passwd.xml

The NULL byte (%00) is just another char to Java, so the malicious value passes the endsWith() check. However, when the value is passed to the operating system’s native API, the NULL byte will represent an end-of-string character, and open the attacker’s intended file. Note that Null byte injection in Java was fixed in Java 7 Update 45. So, make sure you are using at least this version of Java, in addition to validating the user’s input to this File accessor code.

To prevent attacks like this, any of the following steps could help:

  • Use maps to filter out invalid values. Instead of accepting input like file=string , accept file=int . That int can be a key in a Map that points to an allowed file. If the map has no corresponding value for the key given, then throw an error.
  • Strongly validate the file value. Validate the file using an allowlist or regular expression:

Pattern p = Pattern.compile("[1]+\.xml$"); String statement = request.getParameter(“statement”); /* Validate the statement to prevent access to anything but XML files */ if( !p.matcher(statement).matches() ) { response.sendError(404); return; } // Read the file here as normal

Please refer to task number T162: Validate pathname before retrieving local resources. SDElments https://c24.sdelements.com/library/tasks/T162/

  • First Detected 5 days ago
  • Last Detected an hour ago
  • Application Versions 16.0.0-2-beb2f6c | 16.0.0-3-a92ec7d
  • Reported by these servers

Latest version reported - 3.12.0.25978

Latest version reported - 3.12.0.25978

  • Route

    • org.flowable.content.rest.service.api.content.ContentItemCollectionResource.createContentItem(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)


  1. A-Za-z0-9 ↩︎

@joram Could you kindly guide on what is best way to remediate this vulnerability in flowable? Do you recommend any latest version of flowable to help remediate this issue or an alternative approach?

No, as far as I can see the code path is still there.
However, can you describe how this can be used in reality? Each content item gets a unique UUID which is used for the file name - how could this be used?

The only quick-term solution I can see is plugging in your own implementation of ContentStorage (flowable-engine/ContentStorage.java at main · flowable/flowable-engine · GitHub). How are you using the contentService? If you’re not using it - the dependency can also be removed.

The UUID is sent as a form field in the POST:

A malicious actor could hijack the session and replace the processInstanceId with a path of his choosing.

Phil’s response is correct and that is what CONTRAST is stating as a vulerability.

We have added the following code and are waiting if this satisfies CONTRAST

public class FlowableApiInvocationFilter implements Filter {

private static final Logger LOGGER = LogManager.getLogger(FlowableApiInvocationFilter.class);
public static final String FORBIDDEN_MSG = "Not Allowed to Access";

@Override
public void init(FilterConfig filterConfig) throws ServletException {
    LOGGER.info("inside init of Flowable content api");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest)servletRequest;
    HttpServletResponse response = (HttpServletResponse)servletResponse;

    if(request.getContentType() != null && request.getContentType().toLowerCase().contains("multipart/form-data") && request.getMethod().equalsIgnoreCase("post")) {
        LOGGER.info("applying constraint check on content api");
        String pid = request.getParameter("processInstanceId");
        Pattern p = Pattern.compile("^[a-zA-Z0-9-_]+$");
        if (!p.matcher(pid).matches()) {
            LOGGER.info("constraint check failed on content api");
            response.sendError(HttpServletResponse.SC_FORBIDDEN, FORBIDDEN_MSG);
        } else {
            LOGGER.info("constraint check is successful on content api");
            filterChain.doFilter(servletRequest, servletResponse);
        }
    } else {
        filterChain.doFilter(servletRequest, servletResponse);
    }



}

@Override
public void destroy() {
    LOGGER.info("inside destroy of Flowable content api");
}

}

How did that work out for you?