Clickjacking in Adonisjs with Mutual Tls
Clickjacking in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side UI redressing attack where an attacker tricks a user into interacting with a hidden or disguised element inside an embedded frame. AdonisJS applications that render HTML are susceptible if they do not enforce frame-embedding restrictions, regardless of transport security. When Mutual TLS is used, the server validates the client certificate, but this transport-layer assurance does not affect the browser’s behavior regarding framing. As a result, a page served over mTLS can still be loaded inside an iframe or frame on a malicious site if anti-clickjacking headers are absent.
Mutual TLS changes the trust boundary for authentication—both client and server present certificates—but it does not change how the browser parses and renders responses. If an AdonisJS route sets a low Content-Security-Policy frame-ancestors directive or omits X-Frame-Options, an attacker can embed the authenticated mTLS-protected page and overlay interactive elements (e.g., hidden buttons or forms) to capture unintended actions. An mTLS endpoint that returns sensitive UI—such as a confirmation page or a settings form—becomes a target for clickjacking precisely because developers may assume mTLS alone prevents unauthorized interactions.
Consider an AdonisJS application using Mutual TLS for admin endpoints. A route like /admin/confirm-action validates the client certificate and returns an HTML page with a prominent button. Without X-Frame-Options or a strict Content-Security-Policy, an attacker’s page can frame this route. Even though the mTLS handshake ensures the client is authenticated, the framed page can be visually disguised (e.g., styled to look inert) while the attacker listens for clicks via JavaScript overlays or transparent layers. The user, already possessing a valid certificate, unknowingly triggers state-changing requests inside the embedded context.
To detect this with middleBrick, you can submit the mTLS-enabled endpoint (e.g., https://api.example.com/admin/confirm-action) for a scan. The tool checks for missing or weak framing defenses and maps findings to OWASP API Top 10 and related browser security mechanisms. Note that middleBrick performs black-box testing and does not assume internal architecture, so it evaluates the live response headers regardless of whether mTLS was used during the scan.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
Remediation centers on HTTP headers that prevent framing, independent of the transport-layer client certificate validation. For AdonisJS, apply headers globally or per route to ensure framed content is never rendered in an untrusted context.
- Set
X-Frame-OptionstoDENYorSAMEORIGINto instruct browsers not to allow framing. - Use
Content-Security-Policywith a strictframe-ancestorsdirective to whitelist trusted parents or deny all ('none'). - Combine both headers for defense-in-depth, ensuring older and newer browsers are covered.
Below are concrete AdonisJS examples that demonstrate how to enforce these headers while using Mutual TLS.
Global middleware approach
Register a middleware that adds security headers to every response. This is ideal when all routes should reject framing.
// start/kernel.ts
import { middleware } from '@adonisjs/core/http'
export const globalMiddleware = [
middleware.header({
'X-Frame-Options': 'DENY',
}),
middleware.header({
'Content-Security-Policy': "frame-ancestors 'none'",
}),
]
Route-specific approach
Apply headers only to sensitive routes, such as admin or confirmation endpoints that return UI after mTLS authentication.
// start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.get('/admin/confirm-action', async ({ response }) => {
response.header('X-Frame-Options', 'DENY')
response.header('Content-Security-Policy', "frame-ancestors 'none'")
return view.render('admin/confirm')
})
Conditional framing based on origin
If you must allow embedding from specific origins (rare for high-security actions), use a dynamic CSP value. This example allows framing only from the same origin.
// start/routes.ts
import Route from '@adonisjs/core/http'
Route.get('/settings', async ({ request, response }) => {
const csp = `frame-ancestors 'self' ${request.headers().origin || "'none'"}`
response.header('X-Frame-Options', 'SAMEORIGIN')
response.header('Content-Security-Policy', csp)
return view.render('settings')
})
These header-based controls work alongside Mutual TLS, which handles peer authentication at the TLS layer. mTLS ensures the identity of the client, but you must still protect the UI surface against clickjacking through framing restrictions.