[Security] Arbitrary file write via code-generator `tableName` (path traversal)
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.genFile → new 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):
@NotBlank
@ApiModelProperty(value = "表名")
private String tableName;GenConfigController.updateGenConfig — persists unvalidated (GenConfigController.java:46-50):
@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):
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):
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:
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:
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
tableNamebefore 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
Filewhose canonical path escapes it. - Add method-level authorization to the generator controllers if generation should be restricted to privileged roles.
Source: elunez/eladmin