- What: Researchers discovered a vulnerability in Apache FOP that allows sandbox escape
- Impact: Could lead to system compromise via malicious PostScript files
Published on Fri 27 February 2026 by @sigabrt9 Introduction A few months ago, I came across a bug bounty program for an application that uses Apache FOP (Formatting Objects Processor) to generate PostScript files from user supplied XML, then runs GhostScript to generate a PDF. This feature seemed really appealing and very bug prone. For reminder, PostScript is a Turing complete page description language, that can also interact with the underlying system. The complete specification for PostScript can be found here . The most used interpreter for PostScript is the GhostScript project, which can be used on both Windows and Linux. In general, web applications can use GhostScript to perform modifications on PDF files (merging PDF, reducing its size, etc.), to generate PDF from another format or to perform operation on images in the PostScript format. As GhostScript often deals with user supplied input, it implements a sandbox ( -dSAFER enabled by default on recent versions), forbidding access to the underlying operating system. However, even with the sandbox, it is still possible to access the temporary folders (and others) through PostScript and retrieve the content of the files present, which can be, in some contexts, very impactful. Note that, even with the sandbox, it is not recommended to process user-supplied input with GhostScript. Context The target application was using C#/.Net and took user-supplied XML files, combined with a server-side stylesheet (.xsl) resulting in an XSL Formatting object document. Afterwards, Apache FOP was used to transform these files into PostScript files, and finally GhostScript was used to generate and return the final PDF. These Docker files were created as a pwn exercise: they mimic the behavior of the application: the provided name is used in a generated PDF that displays Hello ${name} . The goal is to extract a flag located in /tmp . This issue was found with the help of the Jazzer fuzzer . A simple harness calling Apache FOP to perform transformation from a FO file to PostScript, then running GhostScript on every generated PostScript file and crashing only when GhostScript failed to parse the PostScript file. The bug In the Docker, we can see that sending a simple input will generate a very large PostScript file on the server. For example, the following curl command performed on the docker image: curl -X POST localhost:8081/api/hello -H Content-Type: application/xml --data "<?xml version=\"1.0\" encoding=\"UTF-8\"?><person><prenom>Almond</prenom></person>" Will create the following PostScript file, which can be access through the Docker image: %!PS-Adobe-3.0 %%Creator: (Apache FOP Version 2.11) %%CreationDate: 2025-10-15T14:51:12 %%LanguageLevel: 3 %%Pages: (atend) %%BoundingBox: (atend) %%HiResBoundingBox: (atend) %%DocumentSuppliedResources: (atend) %%EndComments %%BeginDefaults %%EndDefaults %%BeginProlog %%BeginResource: procset (Apache XML Graphics Std ProcSet) 1.2 0 %%Version: 1.2 0 %%Copyright: (Copyright 2001-2003,2010 The Apache Software Foundation. License terms: http://www.apache.org/licenses/LICENSE-2.0) %%Title: (Basic set of procedures used by the XML Graphics project \(Batik and FOP\)) /bd { bind def } bind def /ld { load def } bd /GR/grestore ld [ … ] /Helvetica 18 F 1 0 0 -1 187.431 112.199 Tm (Hello Almond) t ET GR GR GS [ 1 0 0 1 56.692 756.851 ] CT GR Showpage [ … ] The content of the PostScript file is mostly function declarations and comments, but the user input ends up in a PostScript string. Obviously, when Apache FOP creates the PostScript file and generates the string, every character that could break outside the string context is escaped. When the string is written, the function PSPainter.writeText() is executed, which then calls PSGenerator.escapeChar() : public static final void escapeChar ( char c , StringBuffer target ) { switch ( c ) { case '\n' : target . append ( "\\n" ); break ; case '\r' : target . append ( "\\r" ); break ; case '\t' : target . append ( "\\t" ); break ; case '\b' : target . append ( "\\b" ); break ; case '\f' : target . append ( "\\f" ); break ; case '\\' : target . append ( "\\\\" ); break ; case '(' : target . append ( "\\(" ); break ; case ')' : target . append ( "\\)" ); break ; default : if ( c > 255 ) { //Ignoring non Latin-1 characters target . append ( '?' ); } else if ( c < 32 || c > 127 ) { target . append ( '\\' ); target . append (( char )( '0' + ( c >> 6 ))); target . append (( char )( '0' + (( c >> 3 ) % 8 ))); target . append (( char )( '0' + ( c % 8 ))); //Integer.toOctalString(i) } else { target . append ( c ); } } } This code should check every character from the user supplied input and add a \ for every character that could close a PostScript string. For example, sending the following: curl -X POST localhost:8081/api/hello -H Content-Type: application/xml --data "<?xml version=\"1.0\" encoding=\"UTF-8\"?><person><prenom>Almond)')\\\/))</prenom></person>" Would result in the following PostScript file:...