8.1 XSS (Cross-Site Scripting)
Resumo
XSS (Cross-Site Scripting) — injeção de JavaScript malicioso na página para roubar cookies, tokens ou executar ações em nome do usuário.
Tipos: Reflected (pela URL), Stored (no banco), DOM-based (pelo JavaScript).
Proteção: Blade
escapa automaticamente, HTMLPurifier para rich text, Content Security Policy, HTTPOnly cookies.
Conteúdo
O que é
O que é: XSS é injetar JavaScript malicioso na página. O atacante pode roubar cookies, tokens e executar ações em nome do usuário.
Tipos de XSS:
- Reflected XSS — pelos parâmetros da URL
- Stored XSS — gravado no banco
- DOM-based XSS — pelo JavaScript
Como funciona
Reflected XSS (pela URL):
// ❌ Código VULNERÁVEL
Route::get('/search', function (Request $request) {
$query = $request->input('q');
return "Resultados da busca: {$query}";
});
// Ataque:
// /search?q=<script>alert('XSS')</script>
// O JavaScript executa
// ✅ PROTEÇÃO: escape
Route::get('/search', function (Request $request) {
$query = htmlspecialchars($request->input('q'), ENT_QUOTES, 'UTF-8');
return "Resultados da busca: {$query}";
});
// Ou via Blade (escapa automaticamente)
return view('search', ['query' => $request->input('q')]);
// No Blade: Resultados: {{ $query }}Stored XSS (gravado no banco):
// ❌ Código VULNERÁVEL
class CommentController extends Controller
{
public function store(Request $request)
{
Comment::create([
'body' => $request->input('body'), // Sem validação
]);
}
public function show(Comment $comment)
{
return view('comments.show', ['comment' => $comment]);
}
}
// No Blade (sem escape)
{!! $comment->body !!} // ❌ Executa o <script>
// O atacante envia:
// body: <script>fetch('/steal-token?token='+document.cookie)</script>
// ✅ PROTEÇÃO 1: Validação
public function store(Request $request)
{
$validated = $request->validate([
'body' => 'required|string|max:1000',
]);
Comment::create($validated);
}
// ✅ PROTEÇÃO 2: Escape no Blade
{{ $comment->body }} // htmlspecialchars() automático
// ✅ PROTEÇÃO 3: Limpar HTML (se precisar de rich text)
use Mews\Purifier\Facades\Purifier;
public function store(Request $request)
{
Comment::create([
'body' => Purifier::clean($request->input('body')),
]);
}DOM-based XSS:
// ❌ JavaScript VULNERÁVEL
const params = new URLSearchParams(window.location.search);
const message = params.get('msg');
document.getElementById('output').innerHTML = message; // Perigoso!
// Ataque:
// ?msg=<img src=x onerror="alert('XSS')">
// ✅ PROTEÇÃO
document.getElementById('output').textContent = message; // SeguroQuando usar
Sempre se proteja de XSS:
- ✅ Qualquer input do usuário
- ✅ Parâmetros da URL
- ✅ Formulários
- ✅ Dados da API
O Laravel protege automaticamente:
- Blade escapa HTML
- Form Request valida
- Tokens CSRF
Exemplo prático
Saída segura no Blade:
{{-- ✅ Escapa automaticamente --}}
<h1>{{ $post->title }}</h1>
<p>{{ $comment->body }}</p>
{{-- ❌ NÃO escapa (só para HTML confiável) --}}
{!! $post->body !!}
{{-- ✅ Rich text seguro --}}
{!! Purifier::clean($post->body) !!}
{{-- ✅ Escape em atributos --}}
<input type="text" value="{{ $user->name }}">
<a href="{{ $url }}">Link</a>
{{-- ❌ PERIGOSO: contexto JavaScript --}}
<script>
var name = "{{ $user->name }}"; // Pode quebrar o JS
</script>
{{-- ✅ Seguro: JSON encode --}}
<script>
var user = @json($user); // Laravel helper
</script>HTMLPurifier para rich text:
composer require mews/purifier// config/purifier.php (publicar)
php artisan vendor:publish --provider="Mews\Purifier\PurifierServiceProvider"
// Uso
use Mews\Purifier\Facades\Purifier;
class PostController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
]);
Post::create([
'title' => $validated['title'],
'body' => Purifier::clean($validated['body']), // Limpeza
]);
}
}
// No Blade
{!! $post->body !!} // Seguro (já foi limpo)Content Security Policy (CSP):
// app/Http/Middleware/AddSecurityHeaders.php
class AddSecurityHeaders
{
public function handle($request, Closure $next)
{
$response = $next($request);
// Bloquear inline scripts
$response->headers->set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
// Prevenir XSS
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Prevenir clickjacking
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
// Bloquear MIME sniffing
$response->headers->set('X-Content-Type-Options', 'nosniff');
return $response;
}
}
// Registrar em Kernel.php
protected $middleware = [
\App\Http\Middleware\AddSecurityHeaders::class,
];HTTPOnly cookies:
// config/session.php
'http_only' => true, // JavaScript não consegue ler o cookie
// Setar cookie na mão
return response('Conteúdo')->cookie(
'token',
$value,
$minutes = 60,
$path = '/',
$domain = null,
$secure = true, // Só HTTPS
$httpOnly = true // Proteção contra XSS
);Sanitizar o input do usuário:
class CommentController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'body' => 'required|string|max:1000',
]);
// Remover tags HTML (se não precisar de rich text)
$body = strip_tags($validated['body']);
// Ou deixar só tags específicas
$body = strip_tags($validated['body'], '<b><i><u><a>');
Comment::create(['body' => $body]);
}
}Proteção no Vue.js / React:
// Vue escapa automaticamente
<div>{{ message }}</div> // Seguro
// v-html é perigoso
<div v-html="message"></div> // ❌ vulnerabilidade XSS
// React escapa automaticamente
<div>{message}</div> // Seguro
// dangerouslySetInnerHTML é perigoso
<div dangerouslySetInnerHTML={{__html: message}} /> // ❌ XSSTeste de XSS:
// tests/Feature/XssTest.php
class XssTest extends TestCase
{
public function test_xss_payload_is_escaped(): void
{
$xssPayload = '<script>alert("XSS")</script>';
$response = $this->post('/comments', [
'body' => $xssPayload,
]);
$comment = Comment::latest()->first();
// Checar se gravou como está
$this->assertEquals($xssPayload, $comment->body);
// Checar se escapou na saída
$response = $this->get("/comments/{$comment->id}");
$response->assertDontSee('<script>', false); // false = não escapar na busca
$response->assertSee('<script>', false); // Versão escapada
}
}Na entrevista
"XSS é injetar JavaScript na página. Tipos: Reflected (pela URL), Stored (no banco), DOM-based (pelo JS). Proteção: Blade escapa sozinho, {!! !!} não escapa. HTMLPurifier para rich text (Purifier::clean()). Content Security Policy bloqueia inline scripts. HTTPOnly cookies impedem roubo via JS. strip_tags() remove HTML. Sempre validar e escapar o input do usuário. @json() para passar dado pro JavaScript com segurança."
Exercícios práticos
Exercício 1: Corrija a vulnerabilidade XSS
O que está errado neste código? Corrija.
Route::get('/search', function (Request $request) {
$query = $request->input('q');
return view('search', compact('query'));
});
// search.blade.php
<h1>Resultados da busca: {!! $query !!}</h1>Solução
// Problema: {!! !!} não escapa HTML. Dá XSS via ?q=<script>alert('XSS')</script>
// Solução 1: Usar {{ }} (escapa automaticamente)
Route::get('/search', function (Request $request) {
$query = $request->input('q');
return view('search', compact('query'));
});
// search.blade.php
<h1>Resultados da busca: {{ $query }}</h1>
// Solução 2: Validação e limpeza
Route::get('/search', function (Request $request) {
$validated = $request->validate([
'q' => 'required|string|max:255',
]);
$query = strip_tags($validated['q']);
return view('search', compact('query'));
});
// search.blade.php
<h1>Resultados da busca: {{ $query }}</h1>Exercício 2: Implemente um editor rich text seguro
Crie um controller para salvar HTML de um editor WYSIWYG com proteção contra XSS.
Solução
// 1. Instalar HTMLPurifier
composer require mews/purifier
// 2. Publicar o config
php artisan vendor:publish --provider="Mews\Purifier\PurifierServiceProvider"
// 3. PostController
use Mews\Purifier\Facades\Purifier;
class PostController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
]);
// Limpar HTML de tags perigosas
$cleanBody = Purifier::clean($validated['body'], [
'HTML.Allowed' => 'p,b,i,u,a[href],ul,ol,li,strong,em',
]);
$post = Post::create([
'title' => $validated['title'],
'body' => $cleanBody,
'user_id' => auth()->id(),
]);
return redirect()->route('posts.show', $post);
}
}
// 4. No Blade (seguro, já foi limpo)
<div class="post-body">
{!! $post->body !!}
</div>
// 5. config/purifier.php (configuração)
return [
'default' => [
'HTML.Allowed' => 'p,b,i,u,a[href|title],ul,ol,li,strong,em,h2,h3',
'AutoFormat.RemoveEmpty' => true,
],
];Exercício 3: Adicione Content Security Policy
Implemente um middleware para adicionar headers CSP.
Solução
// app/Http/Middleware/ContentSecurityPolicy.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
// Header CSP
$csp = [
"default-src 'self'",
"script-src 'self' https://cdn.jsdelivr.net",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self'",
"frame-ancestors 'none'",
];
$response->headers->set('Content-Security-Policy', implode('; ', $csp));
// Headers de segurança extras
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
return $response;
}
}
// Registro em app/Http/Kernel.php
protected $middleware = [
// ...
\App\Http\Middleware\ContentSecurityPolicy::class,
];
// Alternativa: usar o pacote
composer require spatie/laravel-csp
// config/csp.php
return [
'enabled' => env('CSP_ENABLED', true),
'policy' => [
'default-src' => ['self'],
'script-src' => ['self', 'https://cdn.jsdelivr.net'],
'style-src' => ['self', 'unsafe-inline'],
],
];Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate