Skip to content

Forms & Antiforgery

Key Points

  • Razor <form method="post"> auto-includes anti-forgery token in MVC/Razor Pages projects.
  • [ValidateAntiForgeryToken] on action / OnPost handler validates. Default for Razor Pages POST.
  • File uploads: IFormFile; size limits (Kestrel + middleware); stream large files; never trust filename.
  • Model binding drives form data → C# objects with validation. Tag Helpers handle the HTML side.
  • See Antiforgery & CORS, Model Binding & Validation.

Concepts (deep dive)

Standard form

<form asp-action="Submit" method="post">
    <div>
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">Send</button>
</form>

Tag Helpers generate name attributes, anti-forgery token, validation scripts.

Auto antiforgery in Razor Pages

public class IndexModel : PageModel
{
    [BindProperty] public InputModel Input { get; set; } = new();

    public IActionResult OnPost()
    {
        if (!ModelState.IsValid) return Page();
        // ... save
        return RedirectToPage("/Confirm");
    }
}

Razor Pages auto-validate antiforgery on POST. No [ValidateAntiForgeryToken] needed.

MVC explicit attribute

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Submit(InputModel m) { /* ... */ }

Or globally:

builder.Services.AddControllersWithViews(o =>
    o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

All POST actions auto-validate.

File upload

<form asp-action="Upload" method="post" enctype="multipart/form-data">
    <input asp-for="File" type="file" />
    <button>Upload</button>
</form>
public class UploadVm
{
    public IFormFile? File { get; set; }
}

public async Task<IActionResult> Upload(UploadVm vm)
{
    if (vm.File is null || vm.File.Length == 0) return BadRequest();

    // ❌ Never trust the filename for file system
    // var path = Path.Combine("/uploads", vm.File.FileName);   // path traversal

    // ✅ Sanitize / generate
    var safeName = $"{Guid.NewGuid()}{Path.GetExtension(vm.File.FileName)}";
    var path = Path.Combine("/uploads", safeName);

    await using var stream = System.IO.File.Create(path);
    await vm.File.CopyToAsync(stream);
    return Ok();
}

Multiple files

<input asp-for="Files" type="file" multiple />
public List<IFormFile> Files { get; set; } = new();

Size limits

builder.Services.Configure<FormOptions>(o =>
{
    o.MultipartBodyLengthLimit = 100 * 1024 * 1024;   // 100 MB
});

// Per-action override
[RequestSizeLimit(100_000_000)]
public IActionResult Upload(IFormFile f) { /* ... */ }

Also Kestrel level:

builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 100 * 1024 * 1024);

Streamed (large files)

For very large files, stream rather than buffer in memory:

public async Task<IActionResult> StreamUpload()
{
    var reader = new MultipartReader(...);
    while ((var section = await reader.ReadNextSectionAsync()) is not null)
    {
        // process each section as stream
    }
    return Ok();
}

IFormFile buffers; for streaming, parse the multipart yourself or use MultipartReader.

Validation summary

<div asp-validation-summary="ModelOnly" class="text-danger"></div>

Modes: - All: all errors. - ModelOnly: only ModelState errors not tied to fields. - None.

Client-side validation scripts

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Includes jQuery validation. Modern alternative: skip jQuery, use HTML5 + minor JS.

Custom validators

DataAnnotations:

public class StrongPasswordAttribute : ValidationAttribute { /* ... */ }

public class RegisterVm
{
    [StrongPassword]
    public string Password { get; set; } = "";
}

Or FluentValidation.

CSRF and JS interactions

For server-rendered forms, anti-forgery handles CSRF.

For mixed (server + JS POSTs):

<input type="hidden" name="__RequestVerificationToken" value="@(Antiforgery.GetTokens(HttpContext).RequestToken)" />
fetch('/api/x', {
    method: 'POST',
    headers: { 'RequestVerificationToken': token },
    /* ... */
});

File download

return File(stream, "application/pdf", "invoice.pdf");

// or
return PhysicalFile(path, contentType, fileName);

Honeypot / additional anti-bot

Hidden field that should remain empty:

<input asp-for="HoneypotField" style="display:none" />

Bots often fill all fields → flag as spam.

File content validation

public bool IsImage(IFormFile file)
{
    if (file.Length == 0) return false;
    using var stream = file.OpenReadStream();
    var sig = new byte[4];
    stream.Read(sig);
    // Check JPEG, PNG, GIF magic bytes
    return ...;
}

Don't trust extension or content-type — they're client-supplied.

Full PRG (Post-Redirect-Get) example

public IActionResult Submit(InputVm m)
{
    if (!ModelState.IsValid) return View(m);
    _svc.Save(m);
    TempData["Message"] = "Saved";
    return RedirectToAction(nameof(Done));   // PRG
}

public IActionResult Done() => View();

Antiforgery + multi-tab

User opens form in tab A → token T1 issued. Opens another tab → T2 issued. Submits T1 → may fail if validation regenerates per request.

Use <RequestVerificationToken> with persistent cookies; don't regenerate on every GET.


Code: correct vs wrong

❌ Wrong: trust filename

var path = Path.Combine("/uploads", file.FileName);

../../etc/passwd path traversal.

✅ Correct: sanitize

var safe = Path.GetFileName(file.FileName);   // strips path
// or generate GUID name

❌ Wrong: no size limits

// Kestrel default 30 MB; FormOptions default 128 MB
// For 1 GB upload, OOM

✅ Correct: streamed parsing

var reader = new MultipartReader(boundary, Request.Body);

❌ Wrong: validating only client-side

JS-disabled or attacker → no validation.

✅ Correct: server-side

if (!ModelState.IsValid) return View(m);

Design patterns for this topic

Pattern 1 — "PRG for all POST forms"

  • Intent: no double submit on refresh.

Pattern 2 — "Antiforgery global filter"

  • Intent: secure-by-default.

Pattern 3 — "Stream large file uploads"

  • Intent: memory bounded.

Pattern 4 — "Sanitize filenames"

  • Intent: path traversal defense.

Pattern 5 — "Server-side validation always"

  • Intent: can't trust client.

Pros & cons / trade-offs

Pattern Pros Cons
PRG No double submit Extra redirect
Stream upload Bounded memory More code
File buffering Simple OOM risk
Tag Helpers Strongly typed Razor-specific

When to use / when to avoid

  • Always PRG for state-changing forms.
  • Always server validation.
  • Use streamed upload for large files.
  • Avoid client-only validation.

Interview Q&A

Q1. Why anti-forgery on cookie-auth POST? Cookie auto-attached on cross-origin → attacker can submit form on user's behalf. Token mitigates.

Q2. Razor Pages auto-antiforgery? Yes — POST handlers auto-validate. MVC needs [ValidateAntiForgeryToken] or global filter.

Q3. PRG pattern? POST → Redirect → GET. Refresh re-issues GET, not POST. No duplicate submit.

Q4. File upload size limits? FormOptions.MultipartBodyLengthLimit + Kestrel.MaxRequestBodySize. Tune both.

Q5. Path traversal defense? Don't use raw file.FileName. Sanitize via Path.GetFileName or generate.

Q6. Streamed vs buffered upload? Buffered: simple; loaded in memory. Streamed: bounded; for big files.

Q7. Validate file content? Check magic bytes; don't trust extension.

Q8. Tag Helpers vs HtmlHelpers? Tag Helpers: HTML-like syntax (asp-for). Helpers: @Html.* methods. Modern: Tag Helpers.

Q9. ValidationSummary modes? All, ModelOnly, None. ModelOnly shows only non-field errors.

Q10. Multi-tab antiforgery? Same cookie; tokens valid until cookie reset. Don't regenerate per GET.

Q11. CSRF vs antiforgery? Antiforgery is the .NET implementation of CSRF defense.

Q12. Honeypot field? Hidden field; bots fill it; flag as spam. Lightweight bot defense.


Gotchas / common mistakes

  • ⚠️ Trusting filename — path traversal.
  • ⚠️ No size limit — DoS / OOM.
  • ⚠️ Client-only validation.
  • ⚠️ Direct POST handler without PRG — double submit.
  • ⚠️ Forgetting global anti-forgery in MVC.

Further reading