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
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
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:
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
Modes: - All: all errors. - ModelOnly: only ModelState errors not tied to fields. - None.
Client-side validation scripts
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)" />
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:
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
../../etc/passwd path traversal.
✅ Correct: sanitize
❌ Wrong: no size limits
✅ Correct: streamed parsing
❌ Wrong: validating only client-side
JS-disabled or attacker → no validation.
✅ Correct: server-side
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.