Server Side Template Injection in Adonisjs with Jwt Tokens
Server Side Template Injection in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Server Side Template Injection (SSTI) in AdonisJS can be exposed or amplified when applications embed untrusted data into templates that are rendered after JWT-based authentication is processed. SSTI occurs when an attacker can inject template expressions that are evaluated on the server. In AdonisJS, which uses Edge or custom view engines, unsanitized user input placed into template variables or passed into dynamic template includes can lead to arbitrary code execution or information disclosure.
When JWT tokens are used for authentication, the token payload is often decoded and attached to the request context (e.g., ctx.auth.user). If the application places values from the JWT payload directly into a template—especially if those values originate from user-controlled claims or are concatenated into dynamic template syntax—they can become a vector for SSTI. For example, a developer might use a JWT claim such as ctx.auth.user.prefixedName inside an Edge template via @if(userPrefixedName) or interpolate it into a dynamic include path. If the claim contains template-like syntax (e.g., ${process} in certain template dialects or raw template tags in an unsafe rendering pipeline), the server may evaluate it, leading to remote code execution or sensitive file reads.
Another scenario involves deserialization or dynamic helper registration influenced by JWT claims. If the application dynamically registers template helpers or filters based on values present in the JWT (such as roles or permissions), an attacker who can influence those claims might register a malicious helper that executes code during template rendering. Because JWTs are often trusted after signature verification, developers may skip additional validation of the payload content, inadvertently allowing malicious input to reach the rendering layer.
Consider a route that decodes a JWT and passes the payload into a view:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
export default class SessionController {
public async renderProfile({ auth, view, request }: HttpContextContract) {
const user = auth.user
// Dangerous: directly using a JWT-derived value in a template expression
const displayName = request.input('overrideName') || user.username
return view.render('profile', { displayName })
}
}
If the profile.edge template contains something like @{{ displayName }} and the Edge implementation is misconfigured to allow interpolation of dynamic expressions, an attacker who manipulates overrideName could potentially inject template code that gets evaluated. While AdonisJS and Edge typically escape output, misconfigured custom tags or unsafe dynamic includes can bypass these protections when JWT-derived data is treated as trusted template input.
Additionally, if the application uses JWTs to control feature flags or template paths (e.g., selecting a template based on a role claim from the token), an attacker might leverage path traversal or template injection to read files outside the intended directories. Because JWTs are often considered authoritative after validation, their embedded claims may bypass input sanitization checks that would otherwise mitigate SSTI.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
To mitigate SSTI risks when using JWT tokens in AdonisJS, ensure that all data derived from JWT claims is treated as untrusted input and is validated, sanitized, and escaped before being used in templates. Avoid directly interpolating JWT payload values into template logic or dynamic includes. Use strict schema validation for JWT claims and enforce allowlists for expected values.
Below are concrete remediation examples with valid AdonisJS JWT usage and safe template handling.
1. Validate and sanitize JWT claims before using them in templates
Use AdonisJS schema validation to enforce strict shapes for JWT payloads and sanitize string inputs before passing them to views.
import { schema, rules } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
const profileSchema = schema.create({
username: schema.string({ trim: true }, [
rules.minLength(3),
rules.maxLength(50),
rules.regex(/^[a-zA-Z0-9_]+$/), // allow only safe characters
]),
role: schema.enum(['user', 'admin'] as const), // strict allowlist
})
export default class ProfileController {
public async show({ auth, view, request }: HttpContextContract) {
const payload = request.validatedBody(profileSchema)
// Safe: validated and sanitized values passed to template
return view.render('profile', {
username: payload.username,
role: payload.role,
})
}
}
2. Escape output in Edge templates
Always rely on Edge’s default escaping. Do not use triple mustaches {{{ }}} unless you fully trust the source. For JWT-derived strings, use standard double mustaches which HTML-escape content.
<!-- profile.edge -->
<h1>Welcome, {{ username }}</h1>
<!-- role is used only for UI decisions, not executable logic -->
<span class="role" data-role="{{ role }}"></span>
3. Avoid dynamic template paths or includes based on JWT values
Do not construct template paths using values from JWT tokens. If you need role-specific templates, use a static mapping rather than dynamic resolution.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class HomeController {
public async index({ auth, view }: HttpContextContract) {
const role = auth.user?.role
let template = 'home'
// Static mapping instead of dynamic include paths
if (role === 'admin') {
template = 'home_admin'
}
return view.render(template, { user: auth.user })
}
}
4. Do not register template helpers based on JWT claims
Avoid dynamically adding filters or helpers derived from JWT payloads. Register all helpers statically during application bootstrap.
// Safe: register helpers statically in start/app.ts or a dedicated provider
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class AppProvider {
constructor(protected app: ApplicationContract) {};
public register() {
// Example: static helper registration
this.app.container.singleton('Adonis/Core/View', (_, { makeView }) => {
const view = makeView()
view.global('safeFormatDate', (date: string) => {
// Pure formatting function, no eval or dynamic code
return new Date(date).toLocaleDateString()
})
return view
})
}
}
By combining strict JWT claim validation, output escaping, and avoiding dynamic template behavior based on token data, you reduce the attack surface for SSTI while still leveraging JWTs for authentication.