#908·eladmin

[Security] Arbitrary file write via code-generator `tableName` (path traversal)

Author: Ku4D3Created Jun 25, 2026Updated Jun 25, 2026

Severity: High · CWE: CWE-22 (Path Traversal) Affected versions: v2.7

Summary

The code-generator configuration endpoint persists tableName with only a @NotBlank constraint and no path validation. When code generation runs, the persisted tableName is concatenated into the output directory path, and FreeMarker-rendered content is written to the resulting file with new FileWriter(file) — no canonicalization, no .. rejection. A tableName containing ../ escapes the intended generation directory, so an authenticated user can write generated source files to arbitrary locations on the server filesystem.

Vulnerability chain

Stage Component Location
Source / persist GenConfigController.updateGenConfig binds @RequestBody GenConfig.tableName (@NotBlank only) eladmin-generator/.../rest/GenConfigController.java:46-50
Entity GenConfig.tableName — no @Pattern or path validation eladmin-generator/.../domain/GenConfig.java:48-50
Trigger GeneratorController.generatorCode resolves config by tableName and generates eladmin-generator/.../rest/GeneratorController.java:90-104
Path build GenUtil.download builds tempPath from genConfig.getTableName() eladmin-generator/.../utils/GenUtil.java:108-118
Sink (write) GenUtil.genFilenew FileWriter(file) writes rendered content eladmin-generator/.../utils/GenUtil.java:408-414

The generator controllers enforce authentication (a valid JWT via the global security config) but no method-level authorization, so any authenticated user can call them. updateGenConfig stores tableName verbatim. When POST /api/generator/{tableName}/{type} runs, GeneratorController.generatorCode loads the config by tableName and calls into GenUtil.download/generatorCode, which builds the temp output path as SYS_TEM_DIR/eladmin-gen-temp/<tableName>/ and writes each generated file beneath it. A ../ in tableName resolves that path outside the temp root, and genFile writes attacker-influenced (FreeMarker-rendered) content there.

Key code

GenConfig.tableName — only @NotBlank (GenConfig.java:48-50):

java
@NotBlank
@ApiModelProperty(value = "表名")
private String tableName;

GenConfigController.updateGenConfig — persists unvalidated (GenConfigController.java:46-50):

java
@PutMapping
@ApiOperation("修改")
public ResponseEntity<Object> updateGenConfig(@Validated @RequestBody GenConfig genConfig){
    return new ResponseEntity<>(genConfigService.update(genConfig.getTableName(), genConfig),HttpStatus.OK);
}

GenUtil.download — output path built from persisted tableName (GenUtil.java:108-126):

java
public static String download(List<ColumnInfo> columns, GenConfig genConfig) throws IOException {
    String tempPath = SYS_TEM_DIR + "eladmin-gen-temp" + File.separator + genConfig.getTableName() + File.separator;
    Map<String, Object> genMap = getGenMap(columns, genConfig);
    ...
    for (String templateName : templates) {
        ...
        genFile(file, template, genMap);
    }

GenUtil.genFile — sink, writes rendered content with no path confinement (GenUtil.java:408-414):

java
private static void genFile(File file, Template template, Map<String, Object> map) throws IOException {
    Writer writer = null;
    try {
        FileUtil.touch(file);
        writer = new FileWriter(file);   // file path derived from genConfig.getTableName(), no canonicalize/../reject
        template.render(map, writer);

Proof of Concept

Step 1 — persist a tableName containing a traversal sequence:

http
PUT /api/genConfig HTTP/1.1
Host: <eladmin-host>
Authorization: Bearer <JWT>
Content-Type: application/json

{"tableName":"../../opt/app/config","apiAlias":"t","pack":"com.example","moduleName":"m","path":"p","author":"a","prefix":"t_","cover":true}

Step 2 — trigger generation; rendered content is written to the traversal target:

http
POST /api/generator/../../opt/app/config/0 HTTP/1.1
Host: <eladmin-host>
Authorization: Bearer <JWT>

(The tableName segment must match the persisted value; the generation type 0 writes files, 2 packages a download.)

Impact

  • Authenticated arbitrary file write. Any authenticated user can write FreeMarker-rendered files (and, with cover=true, overwrite existing ones) to paths outside the intended generation directory — for example dropping a class/resource into an application directory or overwriting configuration.
  • The written content is template-rendered source, so an attacker can also influence the file body through the generator's column/config inputs, increasing the risk of planting executable or loadable artifacts.

Remediation

  • Validate and canonicalize tableName before any use: restrict it to identifier characters via @Pattern (for example [A-Za-z_][A-Za-z0-9_]*), reject /, \, .., and confirm the resolved path stays within the configured generation root.
  • Confine generator output to a fixed, validated base directory and reject any File whose canonical path escapes it.
  • Add method-level authorization to the generator controllers if generation should be restricted to privileged roles.