RO EN

Protecție împotriva CSRF, XSS și Injection în ASP.NET Core

Protecție împotriva CSRF, XSS și Injection în ASP.NET Core
Doru Bulubașa
12 May 2026

Articolele anterioare din serie s-au concentrat pe criptografie și autentificare — certificate, tokens, protocoale. Acum facem o schimbare de registru: cele mai frecvente vulnerabilități din aplicații web reale nu vin din criptografie slabă, ci din input nevalidat, context confundat și încredere implicită în date externe.

CSRF, XSS și Injection ocupă constant primele poziții în OWASP Top 10. Nu pentru că sunt greu de înțeles, ci pentru că sunt ușor de omis când ești focusat pe funcționalitate. Acest articol le acoperă pe toate trei: cum funcționează atacul, ce dai greș dacă nu te aperi, și implementarea completă a apărării în ASP.NET Core.


1. CSRF — Cross-Site Request Forgery

Cum funcționează atacul

CSRF exploatează faptul că browser-ul trimite automat cookie-urile de sesiune la orice request către un domeniu, indiferent de unde vine request-ul. Un atacator construiește o pagină care face un request la aplicația ta în numele utilizatorului autentificat — fără ca acesta să știe.

Scenariu concret:

  1. Utilizatorul e autentificat pe bank.com — sesiunea e activă, cookie-ul e în browser
  2. Utilizatorul vizitează evil.com (un link dintr-un email, de exemplu)
  3. evil.com conține un formular ascuns care face POST la bank.com/transfer
  4. Browser-ul trimite automat cookie-ul de sesiune de la bank.com
  5. Serverul vede un request autentificat valid — și execută transferul
<!-- Pagina atacatorului: evil.com -->
<form action="https://bank.com/transfer" method="POST" id="csrf-form">
  <input type="hidden" name="amount" value="10000" />
  <input type="hidden" name="to" value="attacker-account" />
</form>
<script>document.getElementById('csrf-form').submit();</script>

Apărare: Antiforgery Tokens în ASP.NET Core

Mecanismul standard: serverul generează un token unic per sesiune, îl include în formular, și verifică la fiecare POST că token-ul din formular coincide cu cel din cookie/sesiune. Atacatorul de pe evil.com nu poate citi token-ul din DOM-ul paginii tale (same-origin policy), deci nu îl poate include în request-ul fals.

Configurare în Program.cs

builder.Services.AddAntiforgery(options =>
{
    options.HeaderName   = "X-CSRF-TOKEN";   // pentru AJAX
    options.Cookie.Name  = ".myapp.csrf";
    options.Cookie.HttpOnly = false;         // JavaScript trebuie să citească token-ul
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite     = SameSiteMode.Strict;
});

În Razor Pages — automat

Razor Pages include antiforgery automat pentru orice formular cu method="post":

<!-- Razor Page -->
<form method="post">
    <!-- @Html.AntiForgeryToken() e injectat automat -->
    <input asp-for="Amount" />
    <button type="submit">Transfer</button>
</form>

În MVC Controllers

// Aplică validarea antiforgery global pe toate controller-ele
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

// SAU per-action
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Transfer(TransferDto dto) { ... }

// Exclude explicit unde nu e necesar (API endpoints cu JWT)
[HttpPost]
[IgnoreAntiforgeryToken]
public IActionResult ApiEndpoint([FromBody] Dto dto) { ... }

AJAX — citire token din cookie și trimitere prin header

// JavaScript
function getCsrfToken() {
    return document.cookie
        .split('; ')
        .find(row => row.startsWith('.myapp.csrf='))
        ?.split('=')[1];
}

await fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': getCsrfToken()
    },
    body: JSON.stringify({ amount: 100, to: 'account-123' })
});

SameSite Cookie — apărare complementară

SameSite=Strict pe cookie-ul de sesiune oferă un strat suplimentar: browser-ul nu trimite cookie-ul la request-uri cross-site. Nu înlocuiește antiforgery tokens (are excepții), dar le complementează:

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.Strict;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.HttpOnly = true;
});

API-urile cu JWT nu sunt vulnerabile la CSRF dacă token-ul e stocat în memorie (nu în cookie). Un request cross-site nu poate citi token-ul JWT din memoria JavaScript a altui tab — same-origin policy previne asta. Dacă stochezi JWT în cookie, ești din nou vulnerabil și trebuie antiforgery.


2. XSS — Cross-Site Scripting

Cum funcționează atacul

XSS injectează cod JavaScript malițios în pagina ta, care rulează în browser-ul victimei cu acces deplin la DOM, cookies, localStorage și poate face request-uri autentificate în numele utilizatorului.

Există trei tipuri:

  • Reflected XSS — payload-ul vine din URL/request și e reflectat imediat în răspuns (/search?q=<script>...</script>)
  • Stored XSS — payload-ul e salvat în baza de date și afișat ulterior altor utilizatori (comentarii, mesaje, profiluri)
  • DOM-based XSS — payload-ul e procesat de JavaScript client-side fără a ajunge la server (document.write(location.hash))

Impactul poate include: furt de sesiune, keylogging, redirect spre phishing, modificarea paginii, sau executarea de acțiuni în numele utilizatorului.

Apărare 1: Output Encoding — prima linie

Razor face output encoding automat pentru orice expresie @. Aceasta este apărarea principală:

<!-- SIGUR: Razor encodează automat -->
<p>Bun venit, @Model.UserName!</p>
<!-- Dacă UserName = "<script>alert(1)</script>"
     output: &lt;script&gt;alert(1)&lt;/script&gt; -->

<!-- PERICULOS: Html.Raw dezactivează encoding -->
<p>@Html.Raw(Model.UserDescription)</p>
<!-- Folosește NUMAI pentru conținut HTML de încredere -->

Contexte diferite necesită encoding diferit — Razor gestionează asta automat:

<!-- Context HTML — HTMLEncoder -->
<div>@Model.Name</div>

<!-- Context atribut HTML — HTMLEncoder -->
<input value="@Model.Name" />

<!-- Context JavaScript — JavaScriptEncoder (IMPORTANT!) -->
<script>
    var name = "@Json.Serialize(Model.Name)";
    // Nu: var name = "@Model.Name"; -- insuficient în context JS
</script>

<!-- Context URL -->
<a href="/profile?id=@Uri.EscapeDataString(Model.Id)">Profil</a>

Apărare 2: Content Security Policy (CSP)

CSP este un header HTTP care spune browser-ului exact de unde are voie să încarce resurse (scripturi, stiluri, imagini). Chiar dacă un atacator reușește să injecteze un tag <script>, browser-ul îl va bloca dacă sursa nu e în whitelist.

// Program.cs — adaugă CSP header la toate răspunsurile
app.Use(async (context, next) =>
{
    context.Response.Headers.Append(
        "Content-Security-Policy",
        string.Join("; ",
            "default-src 'self'",
            "script-src 'self' https://cdn.jsdelivr.net",
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
            "font-src 'self' https://fonts.gstatic.com",
            "img-src 'self' data: https:",
            "connect-src 'self'",
            "frame-ancestors 'none'",   // protecție și față de Clickjacking
            "base-uri 'self'",
            "form-action 'self'"
        ));
    await next();
});

Evită 'unsafe-inline' pentru scripturi. Dacă ai scripturi inline în pagini Razor, migrează-le în fișiere .js externe. 'unsafe-inline' pentru script-src anulează mare parte din protecția CSP.

Apărare 3: Sanitizare HTML pentru conținut rich-text

Când utilizatorii pot introduce HTML (editoare WYSIWYG, comentarii cu formatare), nu poți codifica totul — trebuie să sanitizezi: păstrezi tag-urile HTML permise și le elimini pe cele periculoase.

dotnet add package HtmlSanitizer
using Ganss.Xss;

public class HtmlSanitizationService
{
    private readonly HtmlSanitizer _sanitizer;

    public HtmlSanitizationService()
    {
        _sanitizer = new HtmlSanitizer();

        // Permitem doar tag-uri sigure
        _sanitizer.AllowedTags.Clear();
        foreach (var tag in new[] { "p", "br", "strong", "em",
            "ul", "ol", "li", "a", "h2", "h3", "blockquote" })
        {
            _sanitizer.AllowedTags.Add(tag);
        }

        // Permitem atribute sigure
        _sanitizer.AllowedAttributes.Clear();
        _sanitizer.AllowedAttributes.Add("href");
        _sanitizer.AllowedAttributes.Add("class");

        // Permitem doar HTTPS în href
        _sanitizer.AllowedSchemes.Clear();
        _sanitizer.AllowedSchemes.Add("https");
    }

    public string Sanitize(string html) => _sanitizer.Sanitize(html);
}

Apărare 4: Security Headers suplimentare

app.Use(async (context, next) =>
{
    var headers = context.Response.Headers;

    // Previne MIME sniffing
    headers.Append("X-Content-Type-Options", "nosniff");

    // Previne afișarea în iframe (Clickjacking)
    headers.Append("X-Frame-Options", "DENY");

    // Forțează HTTPS (HSTS)
    headers.Append("Strict-Transport-Security",
        "max-age=31536000; includeSubDomains; preload");

    // Controlează informațiile din Referrer header
    headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");

    // Dezactivează funcționalități browser periculoase
    headers.Append("Permissions-Policy",
        "camera=(), microphone=(), geolocation=(), payment=()");

    await next();
});

3. Injection

Injection apare când date furnizate de utilizator sunt interpretate ca instrucțiuni — SQL, comenzi shell, expresii LDAP, XML, etc. Cel mai cunoscut: SQL Injection.

SQL Injection — cum funcționează

// VULNERABIL — concatenare directă
var query = $"SELECT * FROM Users WHERE Username = '{username}'";  
// Dacă username = "admin' OR '1'='1"
// query devine: SELECT * FROM Users WHERE Username = 'admin' OR '1'='1'
// Returnează TOȚI utilizatorii

Mai grav — cu SQL Server:

// username = "admin'; DROP TABLE Users; --"
// query: SELECT * FROM Users WHERE Username = 'admin'; DROP TABLE Users; --'

Apărare 1: Parameterizare — singura apărare corectă

// SIGUR — parametri cu ADO.NET
using var cmd = new SqlCommand(
    "SELECT * FROM Users WHERE Username = @username", connection);
cmd.Parameters.AddWithValue("@username", username);
// Parametrul e tratat ca DATE, nu ca SQL — injecția e imposibilă
// SIGUR — Entity Framework Core (parameterizare automată)
var user = await context.Users
    .Where(u => u.Username == username)
    .FirstOrDefaultAsync();
// EF Core generează automat query parametrizat
// SIGUR — Dapper cu parametri
var user = await connection.QueryFirstOrDefaultAsync<User>(
    "SELECT * FROM Users WHERE Username = @Username",
    new { Username = username });
// PERICULOS — Dapper cu interpolare
var user = await connection.QueryFirstOrDefaultAsync<User>(
    $"SELECT * FROM Users WHERE Username = '{username}'");
// Concatenare = vulnerabilitate, indiferent de librărie

Apărare 2: Raw SQL în EF Core — cu atenție

Când ai nevoie de query-uri complexe în EF Core și folosești FromSqlRaw, parameterizarea e responsabilitatea ta:

// PERICULOS
var users = context.Users
    .FromSqlRaw($"SELECT * FROM Users WHERE Username = '{username}'");

// SIGUR — FromSqlInterpolated (parameterizare automată din interpolări)
var users = context.Users
    .FromSqlInterpolated($"SELECT * FROM Users WHERE Username = {username}");

// SIGUR — FromSqlRaw cu parametri expliciți
var users = context.Users
    .FromSqlRaw("SELECT * FROM Users WHERE Username = {0}", username);

Command Injection

Același principiu se aplică și la comenzi shell. Dacă aplicația ta execută procese externe cu input de la utilizator:

// PERICULOS — input direct în comandă shell
Process.Start("bash", $"-c \"convert {userInput} output.pdf\"");
// userInput = "input.jpg; rm -rf /" = dezastru

// SIGUR — argumente separate, fără shell
var process = new Process
{
    StartInfo = new ProcessStartInfo
    {
        FileName  = "convert",
        // Argumente ca array, nu concatenate în shell string
        ArgumentList = { validatedInputPath, "output.pdf" },
        UseShellExecute = false  // NU trece prin shell
    }
};
process.Start();

LDAP Injection

Dacă folosești LDAP pentru autentificare sau căutare directoare Active Directory:

// PERICULOS
var filter = $"(sAMAccountName={username})";
// username = "*)(|(objectClass=*" bypasses authentication

// SIGUR — escape caractere speciale LDAP
var safeUsername = username
    .Replace("\\", "\\5c")
    .Replace("*",  "\\2a")
    .Replace("(",  "\\28")
    .Replace(")",  "\\29")
    .Replace("\0", "\\00");

var filter = $"(sAMAccountName={safeUsername})";

Path Traversal

Un caz special de injection: input-ul utilizatorului influențează un path de fișier:

// PERICULOS
var filePath = Path.Combine(baseDir, userInput);
// userInput = "../../etc/passwd" → citești fișiere în afara directorului permis

// SIGUR — validare că path-ul final e în interiorul directorului permis
public static string SafeCombine(string baseDirectory, string userInput)
{
    var fullPath = Path.GetFullPath(
        Path.Combine(baseDirectory, userInput));

    if (!fullPath.StartsWith(
        Path.GetFullPath(baseDirectory),
        StringComparison.OrdinalIgnoreCase))
    {
        throw new UnauthorizedAccessException(
            "Path traversal detectat.");
    }

    return fullPath;
}

4. Validare input — principii generale

Dincolo de tehnicile specifice fiecărei vulnerabilități, există principii care reduc suprafața de atac în general:

Validare cu Data Annotations și FluentValidation

public class TransferDto
{
    [Required]
    [StringLength(50, MinimumLength = 3)]
    [RegularExpression(@"^[a-zA-Z0-9\-]+$")]  // whitelist de caractere
    public string ToAccount { get; set; } = default!;

    [Range(0.01, 100_000)]
    public decimal Amount { get; set; }
}

// Program.cs — returnează 400 automat la validare eșuată
builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var errors = context.ModelState
                .Where(e => e.Value?.Errors.Count > 0)
                .ToDictionary(
                    k => k.Key,
                    v => v.Value!.Errors
                        .Select(e => e.ErrorMessage).ToArray());

            return new BadRequestObjectResult(new { errors });
        };
    });

Principiul whitelist vs. blacklist

Întotdeauna validează prin whitelist (ce e permis), nu prin blacklist (ce e interzis). Un blacklist va omite întotdeauna ceva:

// SLAB — blacklist: blochezi variante cunoscute, nu toate
if (input.Contains("<script>") || input.Contains("javascript:"))
    return BadRequest();
// Bypass: <SCRIPT>, <scr\nipt>, <img onerror=...>

// CORECT — whitelist: permiți doar ce știi că e sigur
if (!Regex.IsMatch(input, @"^[a-zA-Z0-9 \-_\.]+$"))
    return BadRequest("Input conține caractere nepermise.");

5. Defense in Depth — mai multe straturi

Nicio tehnică singură nu e suficientă. Modelul corect e apărare în profunzime: mai multe straturi independente, astfel încât eșecul unuia să nu compromită tot sistemul.

Vulnerabilitate Strat 1 Strat 2 Strat 3
CSRF Antiforgery tokens SameSite=Strict pe cookie Verificare Origin/Referer header
XSS Output encoding (Razor automat) Content Security Policy HtmlSanitizer pentru rich-text
SQL Injection Parameterizare / ORM Principiul least privilege pe DB user WAF (Web Application Firewall)
Command Injection Argumente separate (nu shell) Validare whitelist a input-ului Sandbox / container fără privilegii
Path Traversal SafeCombine cu Path.GetFullPath Validare extensie fișier Permisiuni filesystem restrictive

6. Testare automată a vulnerabilităților

Apărarea e incompletă fără testare. Câteva unelte utile:

  • OWASP ZAP — scanner automat de vulnerabilități web, gratuit, cu integrare în CI/CD
  • dotnet-retire — detectează dependințe .NET cu vulnerabilități cunoscute
  • Snyk / Dependabot — alertă automată la vulnerabilități în pachete NuGet
  • Security Code Scan — analizor Roslyn care detectează pattern-uri vulnerabile în cod C# la compile time
# Adaugă Security Code Scan ca analyzer
dotnet add package SecurityCodeScan.VS2019

# Detectează automat: SQL Injection, XSS, Path Traversal, CSRF missing
# în cod C# la build time, ca warning-uri de compilare

Test de regresie pentru XSS — exemplu NUnit

[TestFixture]
public class XssProtectionTests
{
    private readonly HtmlSanitizationService _sanitizer = new();

    [TestCase("<script>alert(1)</script>")]
    [TestCase("<img src=x onerror=alert(1)>")]
    [TestCase("<svg onload=alert(1)>")]
    [TestCase("javascript:alert(1)")]
    [TestCase("<a href=\"javascript:void(0)\" onclick=\"alert(1)\">click</a>")]
    public void Sanitize_RemovesXssPayloads(string maliciousInput)
    {
        var result = _sanitizer.Sanitize(maliciousInput);

        Assert.That(result, Does.Not.Contain("<script"));
        Assert.That(result, Does.Not.Contain("onerror"));
        Assert.That(result, Does.Not.Contain("onload"));
        Assert.That(result, Does.Not.Contain("javascript:"));
    }
}

7. Probleme comune și soluțiile lor

Problemă Cauza probabilă Soluție
AntiForgery token invalid la POST Request vine din alt tab / sesiune expirat / SPA fără header Verifică că SPA trimite X-CSRF-TOKEN header; reîncarcă token-ul după re-autentificare
CSP blochează scripturi legitime Whitelist prea restrictiv sau scripturi inline Folosește Content-Security-Policy-Report-Only mai întâi pentru a vedea ce se blochează fără a strica UI
EF Core generează query vulnerabil FromSqlRaw cu interpolare string Înlocuiește cu FromSqlInterpolated sau parametri expliciți
XSS în conținut JSON returnat de API API returnează HTML neencoded în câmpuri JSON Encodează HTML înainte de stocare SAU la afișare; nu te baza pe clientul API să facă asta
Path traversal în upload de fișiere Filename din formular folosit direct Ignoră filename-ul original; generează un GUID pentru numele fișierului stocat

8. Checklist de producție

  • AutoValidateAntiforgeryTokenAttribute aplicat global pe toate controller-ele MVC
  • ✅ Cookie de sesiune cu HttpOnly=true, Secure=true, SameSite=Strict
  • ✅ Content Security Policy configurat și testat (fără 'unsafe-inline' pentru scripturi)
  • ✅ Security headers: X-Content-Type-Options, X-Frame-Options, HSTS
  • ✅ Zero concatenări de string în query-uri SQL — 100% parametrizat sau ORM
  • Html.Raw() folosit exclusiv pentru conținut generat intern, niciodată pentru input utilizator
  • ✅ HtmlSanitizer pentru orice câmp care acceptă HTML de la utilizatori
  • ✅ Validare whitelist pe toate input-urile cu [RegularExpression] sau FluentValidation
  • ✅ Upload fișiere: filename ignorat, extensie validată, stocare cu GUID
  • ✅ SecurityCodeScan sau echivalent în pipeline CI/CD
  • ✅ OWASP ZAP sau scan automatizat în staging înainte de fiecare release
  • ✅ Principiul least privilege pe userul de bază de date — SELECT/INSERT/UPDATE, niciodată DROP sau admin

Concluzie

CSRF, XSS și Injection sunt vechi de aproape trei decenii și încă domină rapoartele de vulnerabilități. Nu pentru că sunt sofisticate — ci pentru că apar din decizii mici, luate sub presiunea timpului: o concatenare de string „temporară”, un Html.Raw() pentru comodință, un cookie fără SameSite.

ASP.NET Core are tooling excelent pentru toate trei: Razor encodează automat, antiforgery e built-in, EF Core parametrizează implicit. Marea majoritate a vulnerabilităților apar când ocolești aceste mecanisme, nu când le folosești.

Seria continuă cu Secure Blazor WebAssembly — un context în care multe dintre principiile de mai sus se aplică diferit, pentru că codul rulează în browser-ul clientului.