Security Hardening

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-09. A JasperReports template is not a document, it is a program: its expressions are Java source that the engine compiles and runs inside the JVM of the service that fills the report. Most of the security history of the library, and most of the settings on this page, follow from that single fact. The page explains where the library trusts its input, which of those places the 7.0 line has since closed or made configurable, and how to set the configuration of the Bookshelf Billing service (a Spring Boot 4 application on Java 21 that fills the monthly sales report from PostgreSQL) so that a bad template, a bad parameter or a bad serialized file does as little harm as possible.

The settings below were checked three ways: against the changes.txt file of the JasperReports repository, against the configuration reference, and by running them. The examples were compiled and run with Java 21 source level (compiled by a newer JDK with --release 21), JasperReports 7.0.8 with the jasperreports-pdf, jasperreports-fonts and jasperreports-jdt artifacts, and H2 2.4.240 standing in for PostgreSQL. What was not run is stated where it comes up. This page does not repeat how the engine works; that is in JasperReports Architecture & Lifecycle, the expression language is in Expressions, Parameters, Fields & Variables, and the query side is in Data Sources & Query Executers.

Threat model

Four things reach the engine from outside the code of the service, and each is a different kind of input.

  • Template source (.jrxml). It contains expressions, scriptlet class names, query text, resource locations and hyperlink targets. Anything an attacker can put there runs with the privileges of the JVM.

  • Compiled templates and generated reports (.jasper, .jrprint). These are Java serialized objects, and loading one means Java deserialization.

  • Parameter values. They are data, but they flow into query text, resource locations (a logo path, a subreport path) and hyperlink URLs, which makes them a way to reach the three other surfaces.

  • Output consumers. An exported HTML page can carry links an attacker chose, and an exported file can carry active content.

The rest of the page takes the surfaces in the order of how much damage each can do.

Version history of the fixes

The table summarizes what the official change log says about security-relevant changes. It is the only source for the version numbers; the release dates are the ones printed there, which can precede the publication on Maven Central: 7.0.8 is dated 2026-07-07 in the change log, and its artifacts were published on Maven Central on 2026-08-05.

Version (date in change log) Item What the change log says

6.13.0 (2020-06-22)

Expression class whitelist

A secure report execution infrastructure that filters the classes used in expressions against a configurable whitelist.

7.0.4 (2025-11-03)

CVE-2025-10492

A deserialization class filter was added to fix the vulnerability.

7.0.7 (2026-05-30)

CVE-2026-6009, repository URL filter

A deserialization class filter was added to fix the vulnerability, and a URL whitelist filter was introduced for controlling access to repository resources.

7.0.8 (2026-07-07)

Query executer switches, hyperlink filter

Configuration properties to disable query executers and to disable dynamic queries that use $P!{} parameter references, and an HTML hyperlink whitelist filter to control remote website access from HTML exports.

The configuration reference gives the query executer and $P!{} switches as Since: 7.0.7, while the change log lists them under 7.0.8. The two sources disagree by one release, and this page does not try to settle it. A service that depends on those switches should be on 7.0.8 or later, where both sources agree they exist, and should confirm the behavior with a test like the ones below, because a property the engine does not know is silently ignored.

The two CVEs

Both entries describe a Java deserialization vulnerability in the Jaspersoft library that can lead to remote code execution. The details below are what the National Vulnerability Database (NVD) record showed on the baseline date.

CVE-2025-10492 CVE-2026-6009

NVD publication date

2025-09-16

2026-05-19

Description

Improper handling of externally supplied data may allow attackers to execute arbitrary code remotely on systems that use the affected library.

Java deserialization vulnerability in the Jaspersoft Reports library leading to remote code execution.

Affected versions of the community library in NVD

Up to and including 7.0.3

No affected-version data was recorded in NVD on the baseline date.

Fix according to the change log

7.0.4

7.0.7

Severity listed in NVD

CVSS 4.0 base score 8.7, High (network, low complexity, low privileges, no user interaction)

CVSS 4.0 base score 8.7, High (same vector)

For CVE-2026-6009 the NVD entry did not list affected versions at the time of checking, so the only version statement available is the fix version in the change log. The change log describes both fixes in the same words (a deserialization class filter), and the public records do not say what the second fix covers beyond the first. This page therefore does not describe the exploit paths of either CVE. The practical conclusion is the same for both and does not depend on the details: run 7.0.8 or later, keep the deserialization filter enabled, and never deserialize files that came from outside the trust boundary.

Because these records change, check the two entries in NVD again before relying on this table, and use the official JasperReports change log as the authority on which release contains which fix.

Untrusted JRXML is code execution

A JRXML file contains expressions such as $F{amount} or "Total: " + $V{total}. During compilation the engine turns each expression into a Java class, and with the default settings nothing restricts which classes the expression may use. To confirm it, the following template was compiled and filled with default settings. Its only element prints the value of an ordinary JVM system property; it could just as well have called Runtime, opened a socket or read a file.

exec.jrxml
<jasperReport name="exec" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <title height="30">
    <element kind="textField" x="0" y="0" width="515" height="26">
      <expression><![CDATA[System.getProperty("user.name")]]></expression>
    </element>
  </title>
</jasperReport>

Filling it with default settings printed the value of the property. That is the intended behavior of the library, not a defect: expressions are a programming language. The consequence is a rule with no exceptions in this section: a template that a user can write or modify is a program that the user can run on your server. Everything else on this page reduces the blast radius; none of it makes it safe to fill templates written by strangers on a shared JVM.

The same applies to scriptlets and extensions (see Scriptlets & Extensions), whose class names come from the template and which are loaded by the engine, and to the language attribute, which selects the expression compiler. Report authors should be treated as developers of the service, with review, version control and a build pipeline, not as end users.

The expression class whitelist

Since 6.13.0 the engine can restrict which classes expressions may touch. Two properties control it. The flag net.sf.jasperreports.report.class.filter.enabled defaults to false, and the whitelist is a set of properties whose names start with net.sf.jasperreports.report.class.whitelist. and end in an arbitrary suffix; each value is a comma separated list of class names. A single * stands for any class or package name or part of one within a package, and ** for any fully qualified name, so net.sf.jasperreports.engine.** allows the engine package and its subpackages. The reference says the flag must be set for the whitelist to apply, and that whitelisting everything with ** costs performance compared with leaving the flag off.

When the flag was enabled and the whitelist contained only String, Integer, BigDecimal and the engine packages, filling the template above failed with the message Class java.lang.System is not visible to reports. The check runs when the expression is evaluated at fill time. The whitelist is a useful second line behind review and build controls. It is not a sandbox: it constrains the classes an expression names, it was verified here for that one case, and it does nothing about a template that only needs allowed classes to do harm, such as a very long loop. For the resource exhaustion side the engine has governors for the number of pages and the elapsed time (net.sf.jasperreports.governor.max.pages.enabled and net.sf.jasperreports.governor.timeout.enabled, both off by default). Their scope includes the report, which means a template can carry its own value; this page did not test whether a template can override a global governor, so do not use the governors as a defense against hostile templates.

Deserialization and compiled templates

A .jasper file, a .jrprint file and the streams that JRLoader reads are Java serialized object graphs. Since 7.0.4 the engine filters the classes that may appear in such a stream, and the filter is controlled by:

  • net.sf.jasperreports.deserialization.class.filter.enabled, default true.

  • net.sf.jasperreports.deserialization.class.whitelist. followed by an arbitrary suffix, a comma separated list of extra classes (same wildcard syntax as above) that may be deserialized.

  • net.sf.jasperreports.deserialization.byte.count.limit, a maximum number of bytes read from the stream; unset means no limit.

  • net.sf.jasperreports.value.deserialization.class.whitelist. followed by a suffix, for values deserialized inside reports; by default no classes are whitelisted for that purpose.

All of these are described as "Since 7.0.4" in the configuration reference except the value whitelist, whose version this page did not check. To see the filter work, a harmless AtomicInteger and a compiled JasperReport were each serialized and read back with JRLoader.loadObject. The first was refused with Class java.util.concurrent.atomic.AtomicInteger is not visible to deserialization, and the second loaded normally. Nothing malicious was used in this check, and no attempt was made to reproduce the exploit of either CVE.

The filter is a safety net for the classes that may appear in a stream, not a promise that a .jasper file from an unknown source is harmless: a compiled template contains the compiled expressions, so loading one and filling it is as dangerous as filling its source. The rule that follows is simple. Compile the templates of the service during the build, package the result, and load only those. Do not accept .jasper or .jrprint uploads, do not read them from a shared directory that other tenants can write, and do not keep serialized reports in a cache that untrusted code can write to.

Resources, repositories and URLs

A template refers to images, subreports, styles and fonts by location, and the engine resolves a location through repository services. The default repository service tries the file system, URLs and the classpath. That is convenient and also the path by which a template (or a parameter that feeds a location expression) can make the server read a local file or call a remote address.

Three switches limit it.

  • net.sf.jasperreports.default.file.repository.enabled (since 6.11.0, default true). When false, the default repository stops resolving locations as local files and uses only URLs and the classpath.

  • net.sf.jasperreports.repository.url.filter.enabled (since 7.0.7, default false). When true, only URLs that match a pattern defined by a property whose name starts with net.sf.jasperreports.repository.url.whitelist. are allowed. Other URLs are rejected and treated as not found.

  • net.sf.jasperreports.svg.allow.external.resources (since 7.0.1, no default documented). It decides whether external resources referenced from inside SVG images are loaded.

The configuration reference does not describe the syntax of the URL whitelist values, so the following behaviors were observed by running RepositoryUtil.getBytesFromLocation against 7.0.8, and should be re-checked after an upgrade.

  • The values are Java regular expressions. A value such as https://assets\.bookshelf\.example/.* is accepted; a glob-style value ending in two asterisks (/**) was reported by the engine as an invalid whitelist pattern and ignored.

  • The pattern is matched against the URL in the form the engine normalizes it to. Reading file:///etc/hosts was matched as file:/etc/hosts, so a pattern that spelled the URL with three slashes did not match, and one that began with file:/etc/ did.

  • With the filter on and no pattern defined, a file: URL was rejected with "Byte data not found".

  • The URL filter does not apply to a plain path such as /etc/hosts without a scheme: with the URL filter enabled it was still read. Only turning off the file repository made it fail.

  • Classpath resources were still found with the filter on.

So the two settings are complementary. Disable the file repository so that plain paths cannot be read, and turn on the URL filter so that only the hosts you control can be fetched. Better still, do not build resource locations from request data at all.

Queries and SQL injection

In a query, $P{customer} becomes a JDBC placeholder and the value is bound by the driver, while $P!{customer} is pasted into the query text. The second form exists for things a placeholder cannot express, such as a column list or an ORDER BY, and it is an SQL injection vector whenever the value is not a constant chosen by the code. The two templates below differ in one expression, and they were run against an invoice table with two rows (INV-1 for Ada and INV-2 for Grace) and the payload x' OR '1'='1.

invoices.jrxml
<jasperReport name="invoices" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <parameter name="customer" class="java.lang.String"/>
  <query language="sql"><![CDATA[SELECT number, customer FROM invoice WHERE customer = $P{customer}]]></query>
  <field name="number" class="java.lang.String"/>
  <field name="customer" class="java.lang.String"/>
  <detail>
    <band height="20">
      <element kind="textField" x="0" y="0" width="150" height="18">
        <expression><![CDATA[$F{number}]]></expression>
      </element>
      <element kind="textField" x="150" y="0" width="300" height="18">
        <expression><![CDATA[$F{customer}]]></expression>
      </element>
    </band>
  </detail>
</jasperReport>

The unsafe variant, invoices-unsafe.jrxml, has name="invoicesUnsafe" and the query SELECT number, customer FROM invoice WHERE customer = '$P!{customer}'; everything else is the same. The observed results:

Run Rows printed

Bound $P{customer} with the injection payload

0

Text $P!{customer} with the injection payload

2 (both customers' invoices leaked)

Text $P!{customer} after disabling the clause

Fill fails: Query parameter clause $P!{customer} is not allowed.

Bound $P{customer} after disabling the clause, with Ada

1

The property that produces the failure is net.sf.jasperreports.query.parameter.clause.enabled. It defaults to true and, when false, any use of the $P!{} syntax in a query results in an error. Set it to false in every service that does not need dynamic query text. A service that does need it (for example to choose the sort column) should map a small set of constants in the Java code and pass the chosen constant as the parameter value, never a request value. The same page that describes the clause is Expressions, Parameters, Fields & Variables. Whether the clause is also reachable through other parameter syntaxes was not examined here.

Disabling query executers

Every <query language="..."> is run by a query executer factory registered for that language: sql for JDBC, and with the extension jars on the classpath xPath, csv, json, jsonql and others. A template can pick any language that is registered, so a service that only ever needs SQL can switch the others off. The property is net.sf.jasperreports.query.executer.factory.{language}.enabled (default true for every language). Running a report with net.sf.jasperreports.query.executer.factory.sql.enabled set to false failed with Query executer for the "sql" language is disabled.

The language names in the default configuration of the core jar come in two spellings for the JDBC factory (sql and SQL) and for XPath (xPath and XPath), CSV (csv and CSV), JSON (json and JSON) and JSONQL (jsonql and JSONQL). Because the property name includes the language string, disable each spelling you do not use. This page ran the sql switch only; the other names are taken from the default properties file of the 7.0.8 jar and the switch was not exercised for each of them. Languages added by other modules (for example Hibernate or EJBQL) were not listed and are not covered here.

Disabling a query executer does not disable a data source that application code builds itself and passes to fillReport: the switch only restricts what a template can ask for by language. That is the intent, since code is trusted and templates are not.

PDF is the subject of this section, but a service that also exports HTML (for example for an in-browser preview of an invoice) copies hyperlink targets from the template and its parameters into href attributes. If the target comes from data, that is a way to publish an attacker’s link on your pages. Since 7.0.8 the HTML exporter can enforce a whitelist:

  • net.sf.jasperreports.export.html.href.filter.enabled, global only, default false. When true, only URLs that match one of the patterns are allowed in the output, and any other URL makes the export fail with an exception. With the flag on and no patterns defined, no URL at all is allowed. The check covers every resolved link, including the anchors and page links the exporter generates itself.

  • net.sf.jasperreports.export.html.href.allow.pattern. followed by an arbitrary suffix. Each value is a Java regular expression matched against the entire href.

The template used for the check is the smallest possible link. Note the attribute linkType: an attribute spelled hyperlinkType, as in older JRXML, compiles without an error and produces no link at all, which would make the test pass for the wrong reason (see Hyperlinks, Bookmarks & Report Books).

link.jrxml
<jasperReport name="link" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <title height="30">
    <element kind="textField" x="0" y="0" width="515" height="26" linkType="Reference">
      <expression><![CDATA["Open"]]></expression>
      <hyperlinkReferenceExpression><![CDATA["https://evil.example/phish"]]></hyperlinkReferenceExpression>
    </element>
  </title>
</jasperReport>

Exporting it to HTML without the filter produced an a element pointing to the example address. With the filter on and the single allowed pattern https://docs\.bookshelf\.example/.*, the export failed with HTML export href URL "https://evil.example/phish" is not allowed. The property is described only for the HTML exporter. This page did not test whether the PDF exporter honors it, and it should not be assumed to.

XML and SVG settings

Two further settings concern parsers inside the library.

  • net.sf.jasperreports.xml.allow.doctype (since 6.1.2, default false). The reference describes it as a security property: with the default, the XML parser used by the library rejects documents containing a DOCTYPE declaration, which prevents XML external entity (XXE) attacks. Leave it false. If an XML data source genuinely needs a DTD, the reference says to enable it only where the XML is known to be safe.

  • net.sf.jasperreports.svg.allow.external.resources (since 7.0.1). Leave it false so that an SVG in a template or parameter cannot pull in external files or addresses.

Neither was exercised by an attack here; they are listed because the official reference calls out their purpose.

A hardened configuration for the Bookshelf Billing service

The settings are read from jasperreports.properties at the root of the classpath, which for a Spring Boot service means src/main/resources/jasperreports.properties. The same keys can also be set in code with DefaultJasperReportsContext.getInstance().setProperty(key, value), which is how the checks above were done. Which of the two is simpler depends on whether you want the settings reviewable as a file; the engine reads both.

src/main/resources/jasperreports.properties
# Deserialization of .jasper / .jrprint (on by default since 7.0.4; keep it explicit)
net.sf.jasperreports.deserialization.class.filter.enabled=true

# Query hardening
net.sf.jasperreports.query.parameter.clause.enabled=false
net.sf.jasperreports.query.executer.factory.xPath.enabled=false
net.sf.jasperreports.query.executer.factory.XPath.enabled=false
net.sf.jasperreports.query.executer.factory.csv.enabled=false
net.sf.jasperreports.query.executer.factory.CSV.enabled=false
net.sf.jasperreports.query.executer.factory.json.enabled=false
net.sf.jasperreports.query.executer.factory.JSON.enabled=false
net.sf.jasperreports.query.executer.factory.jsonql.enabled=false
net.sf.jasperreports.query.executer.factory.JSONQL.enabled=false

# Classes that expressions may use
net.sf.jasperreports.report.class.filter.enabled=true
net.sf.jasperreports.report.class.whitelist.bookshelf=java.lang.String,java.lang.Integer,java.lang.Long,java.lang.Boolean,java.math.BigDecimal,java.util.Date,net.sf.jasperreports.engine.**

# Resource loading
net.sf.jasperreports.default.file.repository.enabled=false
net.sf.jasperreports.repository.url.filter.enabled=true
net.sf.jasperreports.repository.url.whitelist.assets=https://assets\\.bookshelf\\.example/.*

# HTML exports
net.sf.jasperreports.export.html.href.filter.enabled=true
net.sf.jasperreports.export.html.href.allow.pattern.docs=https://docs\\.bookshelf\\.example/.*

A properties file needs the backslash of a regular expression doubled, which is why the dots are written \\.. Placed on the classpath, this file was picked up by a fresh JVM, and the invoices template from above still filled correctly with it. The four failures reported earlier (an expression that uses System, a $P!{} query, a read of a local file path and an HTML link to an address outside the whitelist) were reproduced with the same file. The whitelist for expressions must grow with your templates: the example allows only basic value types and the engine packages, and any report that formats dates with java.time or calls a helper class of the application fails until the class is added, which is the intended effect.

The jasperreports-jdt artifact used to compile expressions at run time pulls a Java compiler into the service. A service that compiles at build time and ships only .jasper files does not need a compiler at run time and has less to defend. Compiling at build time is one of the three options that JasperReports Architecture & Lifecycle compares (its examples compile at start-up instead), and the build setup with jasperreports-maven-plugin is in Large Reports & Performance.

Keeping templates out of user control

The strongest control is architectural: the user chooses a report by a key and supplies parameter values, and never supplies the template, a path, a class name or a query. The class below is the whole policy in code. It compiles the templates that ship inside the application once, at start-up, and refuses any key it does not know. It was compiled and run: an unknown key such as a path-traversal string threw IllegalArgumentException, and a known key filled normally.

ReportCatalog.java
package bookshelf;

import java.io.InputStream;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;

/** Only templates shipped inside the application can be filled; callers pass a key, never a path. */
public final class ReportCatalog {

    private static final Set<String> KEYS = Set.of("invoices");

    private final Map<String, JasperReport> compiled = new HashMap<>();

    public ReportCatalog() throws Exception {
        for (String key : KEYS) {
            try (InputStream in = ReportCatalog.class.getResourceAsStream("/reports/" + key + ".jrxml")) {
                compiled.put(key, JasperCompileManager.compileReport(in));
            }
        }
    }

    public JasperPrint fill(String key, Map<String, Object> parameters, Connection connection) throws JRException {
        JasperReport report = compiled.get(key);
        if (report == null) {
            throw new IllegalArgumentException("Unknown report: " + key);
        }
        return JasperFillManager.fillReport(report, parameters, connection);
    }
}

Two details of the engine matter when calling it. The parameters argument must be a mutable map: an immutable Map.of is rejected by the engine because it adds the built-in parameters to the map you pass, and the run of the class above confirmed an UnsupportedOperationException for it. Wrap request values in a new HashMap<>(...), as the engine expects. And a Java record cannot serve as a bean for JRBeanCollectionDataSource, which is a data source issue rather than a security one but a reason to prefer plain classes with getters for report rows.

The Maven dependencies are the ones of the first example on the data sources page; for the check on this page the only addition was H2 (com.h2database:h2:2.4.240) for the database.

The official sources behind the configuration examples are the configuration reference and the JasperReports change log.

A template review checklist, for the cases where authors who are not developers must contribute templates:

  • Templates live in the repository, are reviewed like code and reach the service only through the build.

  • No expression, scriptlet, subreport path or image path is taken from a parameter that a caller controls.

  • Query text is constant. Dynamic fragments come from a fixed set of constants chosen in Java, and net.sf.jasperreports.query.parameter.clause.enabled is false.

  • The service account that runs the reports has read-only access to the reporting views it needs and nothing else; this limits the damage of an injection that gets past the controls above. Credentials come from the environment or a secrets manager, never from the template (see AWS Secrets Manager and KMS for one option).

  • The service has no outbound network access except to the hosts it needs, so that a rejected URL is also blocked at the network layer.

  • The Spring Boot application and the reporting libraries are updated when new advisories appear.

If the product truly needs end users to design reports, run the fill in a separate, disposable process or container with no credentials, a read-only file system, no outbound network, and CPU, memory and time limits, and treat its output as untrusted. The settings of this page reduce risk in that setup; they do not turn the in-process model into a sandbox.

For the account and request side of a Spring Boot service (authenticating who may request which report, and limiting what they can pass), see Spring Security and the REST integration in JasperReports Spring Boot Integration.