Server Side Template Injection in Adonisjs with Basic Auth
Server Side Template Injection in Adonisjs with Basic Auth — how this specific combination creates or exposes the vulnerability
Server Side Template Injection (SSTI) in AdonisJS occurs when unsanitized user input is rendered through the framework's template engine (typically Edge), allowing an attacker to inject template code that is executed on the server. When Basic Authentication is used without additional safeguards, the combination can amplify exposure: authentication may be accepted via request headers or query parameters that feed into template-rendering logic, and developer assumptions that authenticated contexts are safe can lead to insufficient input validation.
Consider an AdonisJS controller that renders a dashboard using a username passed from Basic Auth (decoded from the Authorization header) directly into an Edge template:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class DashboardController {
public async render({ auth, view, request }: HttpContextContract) {
const user = auth.getUserOrFail()
// Risk: using request query/body parameters to select a display name
const displayName = request.input('displayName', user.username)
return view.render('dashboard', { username: displayName })
}
}
If the displayName parameter is not strictly validated and is rendered in the template like @{{ username }}, an attacker who supplies a malicious payload (e.g., {{ 'foo' }} {{ resolve('process') }} or engine-specific injection patterns) can cause arbitrary server-side code execution or information disclosure within the template context. Basic Auth itself does not introduce SSTI, but it can create a false sense of security: developers may treat authenticated endpoints as low-risk and skip rigorous input validation or output encoding. Additionally, if route parameters or headers derived from Basic Auth are used to dynamically include templates or partials (e.g., view.render(`widgets/${widgetName}`)), an attacker may achieve path traversal or template injection depending on how the engine resolves paths.
AdonisJS does not inherently protect against SSTI when developer code passes untrusted data into templates. The framework's Edge engine by default escapes variables in @{{ }} syntax, but unsafe constructs such as @include(), custom components, or raw interpolation via dangerously properties can bypass escaping. In a Basic Auth scenario where roles or permissions are derived from the authenticated user and then used in template logic (e.g., showing admin controls), flawed authorization checks combined with injection-prone rendering can lead to privilege escalation or data exposure.
An example attack chain might involve an authenticated user sending:
- Authorization header:
Basic dXNlcjpwYXNz(user:test) - Request:
GET /dashboard?displayName={{ 'foo' }} {{ import('child_process').execSync('id') }}
If the application concatenates user input into dynamic template includes or uses input to determine which partial to render, this could result in unintended code execution. The vulnerability is not in Basic Auth but in how the application integrates authentication context with template rendering, emphasizing the need to validate and sanitize all inputs regardless of authentication method.
Basic Auth-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on strict input validation, output encoding, and avoiding the use of untrusted data in template resolution. For AdonisJS applications using Basic Authentication, ensure credentials are verified via middleware but never directly injected into templates without sanitization.
1. Validate and sanitize all inputs used in templates. Do not pass raw request inputs to view.render. Use an allowlist for expected values and escape output explicitly.
import { schema } from '@ioc:Adonis/Core/Validator'
const dashboardSchema = schema.create({
displayName: schema.string.optional({}, [rules.escape()])
})
export default class DashboardController {
public async render({ auth, view, request }: HttpContextContract) {
const user = auth.getUserOrFail()
const payload = await request.validate({ schema: dashboardSchema })
// Safe: escaped by schema, or use a default that is not user-controlled
const displayName = payload.displayName || user.username
return view.render('dashboard', { username: displayName })
}
}
2. Avoid dynamic template includes based on user input. If you must include templates, map input to a strict set of known templates.
const allowedWidgets = ['profile', 'settings', 'overview']
export default class WidgetController {
public async show({ view, request }: HttpContextContract) {
const name = request.qs().widget
if (!allowedWidgets.includes(name)) {
throw new Error('Invalid widget')
}
// Safe: only pre-approved templates can be rendered
return view.render(`widgets/${name}`)
}
}
3. Use proper output encoding in Edge templates. Prefer @{{ variable }} over !{ variable } (unescaped) and avoid dangerously unless absolutely necessary and content is trusted.
{{/* templates/dashboard.edge */}}
<h1>Welcome, @{{ username }}</h1>
{{/* Never do this with untrusted input: &{! username } */}}
4. Middleware to enforce Basic Auth without exposing raw credentials to templates. Keep authentication data in the auth context and avoid merging it with request query/body before validation.
import { BaseMiddleware } from '@ioc:Adonis/Core/Middleware'
export default class AuthMiddleware extends BaseMiddleware {
public async handle({ auth, request, response, next }: HttpContextContract) {
await auth.check()
// Do not attach raw auth details to request that might be used in templates unsafely
return next()
}
}
5. Example of secure Basic Auth verification. Use AdonisJS built-in auth utilities and avoid exposing the username directly to template logic unless necessary and escaped.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import BasicAuth from '@ioc:Adonis/Addons/BasicAuth'
export default class AuthController {
public async login({ auth, request }: HttpContextContract) {
const { username, password } = request.only(['username', 'password'])
// Use a proper provider; ensure credentials are verified securely
const token = await auth.use('api').attempt(username, password)
return { token }
}
}