Warum nur HTTP-Header zuverlässig vor unsichtbaren iframes schützen
Clickjacking täuscht Nutzer mit einem unsichtbaren iframe über einer echten Seite und lässt sie unbemerkt kritische Aktionen wie Kontolöschung oder Überweisungsbestätigung auslösen. Klassisches JavaScript-Frame-Busting lässt sich umgehen, während X-Frame-Options und die CSP-Direktive frame-ancestors als HTTP-Header zuverlässig verhindern, dass eine Seite überhaupt gerahmt wird.
Inhaltsverzeichnis
- 1. Wie Clickjacking technisch funktioniert
- 2. Reale Angriffsszenarien: Likejacking, Cursorjacking und Opacity-Tricks
- 3. Warum klassisches JavaScript-Frame-Busting scheitert
- 4. Bypass-Techniken: Sandbox, Proxy-Stripping und Double-Framing
- 5. X-Frame-Options: DENY und SAMEORIGIN im Detail
- 6. CSP frame-ancestors als moderner Ersatz
- 7. Beide Header kombinieren: Defense in Depth
- 8. Legitime iframe-Einbettungen sauber erlauben
- 9. Testen, ob eine Seite clickjacking-anfällig ist
- 10. Zusammenfassung
- 11. FAQ
1. Wie Clickjacking technisch funktioniert
Clickjacking, auch UI Redressing genannt, legt eine unsichtbare oder fast unsichtbare Ebene der eigentlichen Zielseite über eine scheinbar harmlose Köderseite. Der Angreifer platziert die Zielseite in einem <iframe> mit opacity: 0 oder opacity: 0.01 und positioniert es exakt über einem Button auf der sichtbaren Köderseite, etwa einem „Gewinnspiel starten"-Button. Klickt das Opfer auf den vermeintlich harmlosen Button, trifft der Klick in Wahrheit einen Button in der unsichtbaren Zielseite darunter, zum Beispiel „Konto löschen" oder „Überweisung bestätigen".
Der Angriff funktioniert deshalb, weil Browser Klick-Events an das oberste sichtbare beziehungsweise das per CSS positionierte Element weiterleiten, unabhängig davon, ob dieses Element für den Nutzer erkennbar ist. Mit position: absolute, z-index und exakt kalkulierten Pixel-Koordinaten lässt sich der iframe so ausrichten, dass der kritische Button des Opfers pixelgenau unter dem sichtbaren Köder-Button liegt. Da der Nutzer die eingeloggte Session der Zielseite im Hintergrund mitbringt, wird die Aktion authentifiziert ausgeführt, ohne dass eine zusätzliche Bestätigung nötig wäre. Genau diese Kombination aus Session-Reuse und visueller Täuschung macht Clickjacking zu einer eigenständigen Bedrohungsklasse, die sich nicht mit klassischem XSS oder CSRF-Schutz allein lösen lässt.
2. Reale Angriffsszenarien: Likejacking, Cursorjacking und Opacity-Tricks
Likejacking war die früheste populäre Clickjacking-Variante: Ein unsichtbarer Facebook-„Like"-Button lag über einem Video-Play-Button, sodass jeder Klick auf „Abspielen" gleichzeitig eine Seite likete, ohne dass der Nutzer es bemerkte. Diese Technik verbreitete sich viral, weil jeder Like-Vorgang die Reichweite der Köderseite weiter erhöhte. Ähnliche Muster gibt es bei Newsletter-Abos, Berechtigungsanfragen für Browser-Erweiterungen oder Social-Media-Freigaben, die im Hintergrund eines harmlos wirkenden Spiels oder Quiz liegen.
Cursorjacking geht einen Schritt weiter: Per CSS wird der sichtbare Mauszeiger visuell verschoben, sodass er an einer anderen Position erscheint als der tatsächliche Klickpunkt des Browsers. Der Nutzer glaubt, einen bestimmten Button zu treffen, während der reale Klick einige Pixel daneben auf ein verstecktes Element fällt. Bei Opacity-Tricks wird der iframe nicht komplett unsichtbar gemacht, sondern nur leicht transparent, kombiniert mit einem passenden Hintergrundbild, das die Illusion einer einzigen zusammenhängenden Seite erzeugt. In Kombination mit gefälschten Formularfeldern lassen sich so auch mehrstufige Aktionen wie Zahlungsbestätigungen oder Berechtigungsdialoge in Admin-Oberflächen kapern, wenn diese ungeschützt in fremde Seiten eingebettet werden können.
3. Warum klassisches JavaScript-Frame-Busting scheitert
Vor der breiten Unterstützung von HTTP-Headern versuchten Entwickler, Clickjacking mit clientseitigem Frame-Busting zu verhindern. Das klassische Muster prüft, ob das aktuelle Fenster das oberste Fenster ist, und leitet andernfalls die übergeordnete Seite auf die eigene URL um. Das Problem: Dieser Code läuft im Browser des Opfers und kann von einem Angreifer, der die Seite in einem iframe einbettet, kontrolliert oder komplett unterdrückt werden, bevor er überhaupt zur Wirkung kommt.
Frame-Busting ist damit strukturell ein Wettrüsten auf der falschen Seite der Vertrauensgrenze. Der Angreifer kontrolliert die umgebende Seite und damit alle Mittel, mit denen das eingebettete JavaScript neutralisiert werden kann, bevor es ausgeführt wird oder bevor seine Wirkung greift. Sicherheitsentscheidungen, die im JavaScript der eingebetteten Seite getroffen werden, sind grundsätzlich angreifbar, weil der Angreifer die Ausführungsumgebung des iframes kontrolliert. Deshalb gilt Frame-Busting seit Jahren als Anti-Pattern und wurde durch serverseitig gesetzte HTTP-Header ersetzt, die der Browser vor dem Rendern der Seite auswertet, unabhängig vom Verhalten des umgebenden Dokuments.
// INSECURE / LEGACY: classic frame-busting - do NOT rely on this
// Shown for educational contrast only, easily bypassed by an attacker
if (top !== self) {
top.location = self.location;
}
// A slightly more defensive variant, still unreliable
(function preventFraming() {
if (top !== self) {
document.body.style.display = 'none';
top.location = self.location;
}
})();
// Both approaches fail against sandboxed iframes, onbeforeunload
// tricks, or a stripping proxy - see section 4 for details.
// The reliable fix is a server-side header, not client-side JS:
// X-Frame-Options: DENY
// Content-Security-Policy: frame-ancestors 'none'
4. Bypass-Techniken: Sandbox, Proxy-Stripping und Double-Framing
Das sandbox-Attribut eines iframes ohne das Token allow-top-navigation verhindert gezielt, dass eingebetteter Code die übergeordnete Seite umleiten kann. Ein Angreifer setzt einfach <iframe sandbox="allow-scripts allow-forms" src="...">, das JavaScript der Zielseite läuft weiterhin, aber der Befehl top.location = self.location wird vom Browser stillschweigend blockiert. Das Frame-Busting-Skript feuert, hat aber keinerlei Wirkung mehr, während der Angriff unbemerkt weiterläuft.
Eine zweite Bypass-Klasse nutzt Proxy-Stripping: Der Angreifer lädt die Zielseite nicht direkt, sondern über einen eigenen Reverse-Proxy, der beim Durchreichen der HTTP-Response gezielt den X-Frame-Options-Header entfernt, sofern die Zielseite sich nicht zusätzlich per CSP schützt. Beim Double-Framing wird die Zielseite in einen zweiten, verschachtelten iframe gepackt, um veraltete Frame-Busting-Skripte zu verwirren, die nur einen einzelnen Verschachtelungslevel prüfen. Zusätzlich existieren onbeforeunload-Tricks, bei denen der Angreifer einen Dialog abfängt, der beim Versuch der Umleitung durch das Frame-Busting-Skript erscheinen würde, und diesen für den Nutzer verschleiert oder automatisch abbricht. Alle diese Techniken zeigen denselben Kernpunkt: clientseitige Gegenmaßnahmen sind grundsätzlich durch die Kontrolle des Angreifers über die Einbettungsumgebung aushebelbar.
5. X-Frame-Options: DENY und SAMEORIGIN im Detail
Der HTTP-Header X-Frame-Options war der erste serverseitige, browserdurchgesetzte Schutz gegen Clickjacking und wird vom Browser ausgewertet, bevor der Frame-Inhalt überhaupt gerendert wird. Der Wert DENY verbietet jede Einbettung der Seite in einem Frame, egal von welcher Origin, selbst von der eigenen Domain. Der Wert SAMEORIGIN erlaubt Framing nur, wenn die einbettende Seite dieselbe Origin hat wie die eingebettete Seite, was für Admin-Oberflächen mit internen iframes oder Multi-Domain-Setups oft die praktikablere Wahl ist.
Die entscheidende Einschränkung von X-Frame-Options: Der Header unterstützt keine Liste erlaubter Origins. Es gibt zwar historisch den Wert ALLOW-FROM uri, dieser wurde jedoch nie zuverlässig in modernen Browsern implementiert und gilt als veraltet. Wer also mehrere spezifische Partnerdomains als Frame-Eltern erlauben möchte, etwa für einen eingebetteten Zahlungs-Widget-Anbieter und ein separates Partnerportal gleichzeitig, kann das mit X-Frame-Options allein nicht abbilden. Genau diese Lücke schließt die CSP-Direktive frame-ancestors, die im nächsten Abschnitt beschrieben wird.
# nginx.conf: send X-Frame-Options for legacy browser coverage
# Place inside the server or location block serving the Magento storefront
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME-sniffing alongside the framing protection
add_header X-Content-Type-Options "nosniff" always;
# Restrictive fallback for admin routes: no framing at all
location /admin_xyz123/ {
add_header X-Frame-Options "DENY" always;
proxy_pass http://magento_upstream;
}
6. CSP frame-ancestors als moderner Ersatz
Die Content-Security-Policy-Direktive frame-ancestors ersetzt X-Frame-Options funktional vollständig und geht deutlich darüber hinaus. Statt nur zwischen „gar nicht", „nur eigene Origin" und einem faktisch nicht existierenden Allowlist-Mechanismus zu wählen, akzeptiert frame-ancestors eine beliebige Liste konkreter Origins: Content-Security-Policy: frame-ancestors 'self' https://partner-a.example https://partner-b.example. Damit lässt sich exakt abbilden, welche Partnerdomains eine Seite einbetten dürfen, ohne alle anderen Domains ebenfalls zuzulassen.
Der Wert 'none' entspricht funktional X-Frame-Options: DENY, der Wert 'self' entspricht SAMEORIGIN. Zusätzlich unterstützt frame-ancestors Wildcard-Subdomain-Muster wie https://*.mironsoft.de, was bei Multi-Store-Setups mit mehreren Subdomains erhebliche Konfigurationsarbeit spart. Wichtig: frame-ancestors kann ausschließlich über den HTTP-Header Content-Security-Policy gesetzt werden, nicht über ein <meta>-Tag im HTML, weil der Browser das Framing-Verhalten entscheiden muss, bevor der Seiteninhalt überhaupt geparst wird.
# .htaccess or Apache vhost config for the Magento pub/ document root
# frame-ancestors replaces X-Frame-Options with origin-list support
<IfModule mod_headers.c>
# Allow framing only from same origin plus two named partner domains
Header always set Content-Security-Policy "frame-ancestors 'self' https://payment-widget.example https://partner-portal.example"
# Legacy header for browsers that ignore frame-ancestors
Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>
7. Beide Header kombinieren: Defense in Depth
Obwohl frame-ancestors der modernere und mächtigere Mechanismus ist, empfiehlt sich das gleichzeitige Senden von X-Frame-Options als zusätzliche Absicherungsebene. Der Grund liegt in der Browser-Kompatibilität: Ältere Browser-Versionen und manche eingebetteten Webviews in mobilen Apps unterstützen frame-ancestors nicht vollständig, werten aber X-Frame-Options zuverlässig aus. Wenn beide Header gesetzt sind und sich widersprechen, gewinnt in modernen Browsern grundsätzlich die restriktivere CSP-Direktive, während ältere Browser auf den klassischen Header zurückfallen.
In der Praxis bedeutet das: X-Frame-Options: SAMEORIGIN zusammen mit Content-Security-Policy: frame-ancestors 'self' https://partner.example deckt sowohl den modernen Anwendungsfall mit mehreren erlaubten Origins als auch die Legacy-Kompatibilität ab. Dieses Prinzip der doppelten Absicherung, auch Defense in Depth genannt, ist in der Sicherheitspraxis Standard: Ein einzelner Kontrollmechanismus, der ausfällt oder in einem Randfall nicht greift, darf niemals die einzige Schutzschicht sein. Für Magento-Shops mit Varnish Full Page Cache ist zusätzlich wichtig, dass beide Header konsistent über alle Cache-Ebenen hinweg an jede Response angehängt werden, nicht nur an dynamisch generierte Seiten.
8. Legitime iframe-Einbettungen sauber erlauben
Nicht jede Einbettung ist ein Angriff. Zahlungsanbieter wie 3-D-Secure-Widgets, PayPal-Buttons oder Klarna-Checkout-Komponenten laufen technisch in einem iframe, das von der eigenen Shop-Domain eingebettet wird, aber umgekehrt muss auch die Zahlungsseite selbst kontrollieren, wer sie einbetten darf. Auch das Magento-Backend nutzt intern iframes, etwa für bestimmte WYSIWYG-Editoren oder Import/Export-Vorschauen, und Partnerprogramme benötigen häufig einbettbare Widgets, etwa Produktbewertungs- oder Preisvergleichs-Komponenten, die auf Drittanbieterseiten dargestellt werden sollen.
Für diese Fälle gilt: Statt Framing komplett zu verbieten, wird frame-ancestors mit einer engen, explizit gepflegten Liste konkreter Origins konfiguriert. Ein generisches * in frame-ancestors sollte vermieden werden, außer bei bewusst öffentlich einbettbaren Widget-Seiten, die keine sensiblen Aktionen enthalten. Für Magento eignet sich ein Plugin auf den HTTP-Response, das je nach Route unterschiedliche frame-ancestors-Werte setzt, etwa restriktiv für /customer/account, aber offener für eine dedizierte Widget-Route wie /widget/pricecompare. Wichtig ist, jede Allowlist-Ergänzung zu dokumentieren und regelmäßig zu prüfen, ob die eingetragene Partnerdomain noch aktiv genutzt wird, da veraltete Einträge unnötige Angriffsfläche offen lassen.
<?php
declare(strict_types=1);
namespace Mironsoft\Security\Plugin;
use Magento\Framework\App\Response\Http;
use Magento\Framework\App\Request\Http as HttpRequest;
/**
* Sets clickjacking-protection headers on every HTTP response,
* with a narrow allowlist for routes that legitimately need framing.
*/
class ClickjackingHeadersPlugin
{
/**
* @var array<string, string[]> Route prefix to allowed frame-ancestors origins
*/
private const ROUTE_ALLOWLIST = [
'widget/pricecompare' => ['https://partner-portal.example'],
'checkout/onepage' => ["'self'", 'https://payment-widget.example'],
];
/**
* @param HttpRequest $request Current HTTP request, used to resolve the route.
*/
public function __construct(private readonly HttpRequest $request)
{
}
/**
* Adds X-Frame-Options and CSP frame-ancestors headers before the response is sent.
*
* @param Http $subject The response object being dispatched.
* @return void
*/
public function beforeSendResponse(Http $subject): void
{
$path = ltrim((string) $this->request->getPathInfo(), '/');
$allowedOrigins = ["'self'"];
foreach (self::ROUTE_ALLOWLIST as $prefix => $origins) {
if (str_starts_with($path, $prefix)) {
$allowedOrigins = $origins;
break;
}
}
$subject->setHeader(
'Content-Security-Policy',
'frame-ancestors ' . implode(' ', $allowedOrigins),
true
);
// Legacy fallback: only DENY/SAMEORIGIN are valid single values
$subject->setHeader(
'X-Frame-Options',
count($allowedOrigins) === 1 && $allowedOrigins[0] === "'self'" ? 'SAMEORIGIN' : 'DENY',
true
);
}
}
9. Testen, ob eine Seite clickjacking-anfällig ist
Der schnellste Test ist eine minimale HTML-Testdatei, die die Zielseite in einem iframe einbettet und lokal im Browser geöffnet wird. Lädt die Zielseite sichtbar innerhalb des Frames, fehlen die Schutz-Header oder sind sie falsch konfiguriert. Verweigert der Browser das Rendern mit einer Konsolenmeldung wie „Refused to display '...' in a frame because it set 'X-Frame-Options' to 'deny'", greift der Schutz wie erwartet. Dieser Test sollte für jede sensible Route einzeln durchgeführt werden, nicht nur für die Startseite, da Header-Konfigurationen je nach Cache-Regel oder Reverse-Proxy variieren können.
Für automatisierte Prüfungen eignen sich curl -I gegen die produktive URL, um die tatsächlich ausgelieferten Response-Header zu inspizieren, sowie Online-Scanner wie die Security-Header-Checks von Mozilla Observatory oder securityheaders.com, die zusätzlich auf fehlende X-Content-Type-Options und andere Header hinweisen. In CI/CD-Pipelines lässt sich ein einfacher Header-Assertion-Test integrieren, der bei jedem Deployment prüft, ob X-Frame-Options und Content-Security-Policy: frame-ancestors auf allen kritischen Routen vorhanden sind, bevor eine Regression unbemerkt in Produktion gelangt.
<!-- clickjack-test.html: local test page to check if a target is framable -->
<!-- Open this file directly in a browser (file://) and observe the console -->
<!DOCTYPE html>
<html lang="en">
<head><title>Clickjacking test</title></head>
<body>
<h1>If the page below renders, it lacks clickjacking protection</h1>
<iframe src="https://shop.mironsoft.de/customer/account/"
sandbox="allow-scripts allow-same-origin"
width="800" height="600">
</iframe>
</body>
</html>
| Ansatz | Durchsetzung | Mehrere Origins | Bewertung |
|---|---|---|---|
| JS Frame-Busting | Client, nach dem Laden | Nein | Unsicher, leicht umgangen |
| X-Frame-Options: DENY | Server, vor dem Rendern | Nein | Sicher, aber starr |
| X-Frame-Options: SAMEORIGIN | Server, vor dem Rendern | Nein | Sicher für Same-Origin |
| CSP frame-ancestors | Server, vor dem Rendern | Ja, Origin-Liste | Empfohlen, moderner Standard |
| Beide Header kombiniert | Server, vor dem Rendern | Ja, plus Legacy-Fallback | Beste Praxis, Defense in Depth |
Die Tabelle macht den zentralen Unterschied deutlich: Nur serverseitig gesetzte Header werden vom Browser ausgewertet, bevor die Seite überhaupt sichtbar wird, während JavaScript im Frame-Kontext immer der Kontrolle des Angreifers unterliegt. Wer heute ein neues Projekt absichert, sollte direkt mit frame-ancestors beginnen und X-Frame-Options ausschließlich als Kompatibilitäts-Fallback ergänzen, niemals als alleinigen Schutz.
Mironsoft
Security-Header, CSP-Konfiguration und Härtung für Magento-Shops
Ist euer Shop gegen Clickjacking abgesichert?
Wir prüfen eure produktiven Response-Header, identifizieren fehlende oder falsch konfigurierte X-Frame-Options- und CSP-Direktiven und setzen eine saubere frame-ancestors-Allowlist für alle legitimen Embedding-Fälle um.
Security-Header-Audit
Vollständige Prüfung aller Routen auf X-Frame-Options und CSP
CSP-Implementierung
frame-ancestors, script-src und weitere Direktiven für Magento und Hyvä
Embedding-Allowlist
Saubere Ausnahmen für Zahlungs-Widgets, Partner und Admin-iframes
10. Zusammenfassung
Der Clickjacking-Schutz für Magento-Shops löst ein klar umrissenes Problem: Ein unsichtbarer iframe über einer echten Seite darf niemals in der Lage sein, Klicks eines Nutzers auf verdeckte, kritische Aktionen umzulenken. JavaScript-Frame-Busting, egal wie ausgefeilt, läuft im Kontrollbereich des Angreifers und ist deshalb prinzipiell umgehbar, etwa über das sandbox-Attribut, header-strippende Proxys oder verschachteltes Double-Framing. Der zuverlässige Schutz kommt ausschließlich vom Server: X-Frame-Options mit DENY oder SAMEORIGIN für einfache Fälle, Content-Security-Policy: frame-ancestors mit einer expliziten Origin-Liste für alle Fälle, in denen mehrere spezifische Partnerdomains eine Einbettung benötigen.
In der Praxis bewährt sich die Kombination beider Header als Defense-in-Depth-Strategie, ergänzt um eine sauber gepflegte Allowlist für legitime Anwendungsfälle wie Zahlungs-Widgets, Partner-Embeds oder interne Admin-iframes. Regelmäßige Tests mit einer minimalen iframe-Testseite oder automatisierten Header-Checks in der CI-Pipeline stellen sicher, dass ein Deployment, ein neuer Reverse-Proxy oder eine Cache-Regel nicht versehentlich den Schutz aushebelt, der über Monate zuverlässig funktioniert hat.
Clickjacking-Schutz für Magento-Shops - Das Wichtigste auf einen Blick
JS-Frame-Busting meiden
Clientseitige Skripte wie if (top != self) sind durch sandbox-iframes und Proxy-Stripping umgehbar. Kein verlässlicher Schutz.
X-Frame-Options setzen
DENY oder SAMEORIGIN als Legacy-Fallback, aber ohne Unterstützung für mehrere Origins.
CSP frame-ancestors nutzen
Moderner Standard mit expliziter Origin-Liste, ersetzt X-Frame-Options funktional vollständig.
Beide Header kombinieren
Defense in Depth für maximale Browser-Kompatibilität, plus gepflegte Allowlist für legitime Embeds.