Security
|
This section documents ASP.NET Web Forms on .NET Framework 4.8.1, the last and permanent version of Web Forms — the page life cycle and postback model, ViewState and control state, server controls, validation controls, master pages and themes, data-bound controls, and the provider-based security model — as described by the official documentation at Microsoft Learn and the ASP.NET previous-versions archive, which are the reference these pages are written and verified against. Web Forms receives security fixes only and has no forward path onto modern .NET (.NET Framework 4.8.1 is Microsoft’s last version of .NET Framework; Web Forms itself never shipped on .NET Core/.NET 5+). It remains supported for existing applications running on Windows but is not recommended for new development — see Choosing an ASP.NET Framework and Migrating to Modern ASP.NET for what that means in practice. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Web Forms security predates ASP.NET Core Identity by roughly a decade and is built around configuration-driven
authentication modules and a swappable provider model rather than middleware and dependency injection — the concepts map onto modern ASP.NET Core Identity (see the concept table in
Choosing an ASP.NET Framework) but the mechanics are distinctly
System.Web-era.
Threat surface and the ASP.NET security context
A Web Forms request carries risk at several layers simultaneously: the transport (HTTPS), the authentication module deciding who the caller is, URL/file authorization deciding what they may reach, ViewState/event validation guarding against tampered postbacks (see ViewState and Control State), and ordinary web-application concerns (XSS, SQL injection, CSRF) that apply to any framework. This page covers the Web-Forms-specific mechanisms; general web hardening is out of this section’s scope.
Forms authentication
FormsAuthentication issues a signed (and optionally encrypted) cookie — the ticket — after a successful
login, and a module checks it on every subsequent request:
<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Login.aspx" timeout="30" slidingExpiration="true"
cookieless="UseCookies" protection="All" name=".ASPXAUTH" />
</authentication>
</system.web>
protected void LoginButton_Click(object sender, EventArgs e)
{
if (Membership.ValidateUser(UsernameBox.Text, PasswordBox.Text))
{
FormsAuthentication.SetAuthCookie(UsernameBox.Text, createPersistentCookie: RememberMeBox.Checked);
Response.Redirect(FormsAuthentication.GetRedirectUrl(UsernameBox.Text, false));
}
else
{
StatusLabel.Text = "Invalid username or password.";
}
}
slidingExpiration="true" reissues the ticket once more than half its timeout has elapsed on an active
request, keeping an active user logged in without extending a session indefinitely past inactivity;
protection="All" both validates (MAC) and encrypts the ticket using machineKey.
Windows authentication (<authentication mode="Windows"/>, paired with IIS-level Windows
Authentication/Negotiate) instead delegates identity entirely to the OS/domain — typical for intranet
line-of-business apps where every user already has a domain account, with no login page at all.
URL authorization and file authorization
<authorization> restricts access declaratively by user/role, and combines with <location> (see
HTTP Pipeline, Handlers, and
Configuration) to scope rules to specific folders:
<location path="Admin">
<system.web>
<authorization>
<allow roles="Administrators" />
<deny users="*" />
</authorization>
</system.web>
</location>
Rules are evaluated top-to-bottom, first match wins — <allow roles="Administrators"/> before
<deny users="*"/> is required, not incidental. File authorization
(FileAuthorizationModule) additionally checks NTFS ACLs against the authenticated Windows identity, relevant
only under Windows authentication.
The provider model
Membership, roles, and profile are each fronted by an abstract provider base class, swappable via configuration without changing calling code:
| Provider base | Built-in implementations |
|---|---|
|
|
|
|
|
|
<system.web>
<membership defaultProvider="SqlMembership">
<providers>
<add name="SqlMembership" type="System.Web.Security.SqlMembershipProvider"
connectionStringName="ASPNETDB" applicationName="/MyApp"
minRequiredPasswordLength="8" requiresUniqueEmail="true" />
</providers>
</membership>
<roleManager enabled="true" defaultProvider="SqlRole">
<providers>
<add name="SqlRole" type="System.Web.Security.SqlRoleProvider"
connectionStringName="ASPNETDB" applicationName="/MyApp" />
</providers>
</roleManager>
</system.web>
if (Roles.IsUserInRole(User.Identity.Name, "Administrators")) { /* ... */ }
MembershipUser user = Membership.GetUser();
Login controls and their events
A family of declarative controls wraps Membership/FormsAuthentication end to end, needing little or no
code-behind for the common cases:
| Control | Purpose |
|---|---|
|
Username/password form; |
|
Shows different markup templates for anonymous vs. authenticated (optionally per-role) users. |
|
A "Log in"/"Log out" link that swaps automatically based on auth state. |
|
Self-service registration against the configured |
|
Email-based password reset flow (requires |
|
Authenticated password-change form. |
<asp:Login ID="LoginControl" runat="server" OnAuthenticate="LoginControl_Authenticate" />
<asp:LoginView runat="server">
<AnonymousTemplate>Please <asp:LoginStatus runat="server" /></AnonymousTemplate>
<LoggedInTemplate>Welcome, <asp:LoginName runat="server" />!</LoggedInTemplate>
</asp:LoginView>
ASP.NET Universal Providers
The "Universal Providers" NuGet package (Microsoft.AspNet.Providers) reimplements
Membership/Role/Profile/SessionState providers against a plain SQL Server (including SQL Server Compact/LocalDB)
schema managed by Entity Framework migrations, avoiding the older aspnet_regsql-provisioned ASPNETDB schema — useful when the hosting environment cannot run aspnet_regsql directly (e.g. many shared/Azure hosts of that
era).
Trust levels
<trust level="…"> constrained what a Web Forms application was permitted to do at the CLR level under
Code Access Security (CAS) — Full, High, Medium, Low, Minimal — historically used by shared hosts
to sandbox tenant applications from each other and from the filesystem/registry. CAS-based trust levels are
obsolete: .NET Framework 4 deprecated partial-trust ASP.NET hosting, and <trust level="Full"> (the only
level guaranteed to work correctly with most modern libraries) is effectively mandatory. This is documented
here because older code and hosting guides still reference <trust level="Medium">; do not attempt to use
partial trust in a current application.
Request validation and ValidateRequest
ASP.NET’s built-in request validation rejects a request outright if any input contains markup that looks like HTML/script, as a baseline XSS defense — on by default since ASP.NET 1.1:
<system.web>
<httpRuntime requestValidationMode="4.5" /> <!-- lazy validation: only checked when the value is actually read -->
</system.web>
<%@ Page ValidateRequest="false" %> <%-- disable only for the specific page that legitimately needs raw HTML input --%>
Disabling ValidateRequest removes a real protection and shifts full responsibility for encoding/sanitizing
that input onto application code — it should never be disabled site-wide, only on the specific page (e.g. a
rich-text editor submission) that needs it, paired with explicit output encoding and, ideally, a
allow-list-based HTML sanitizer on the way in.
Antiforgery and CSRF
Web Forms has no first-class AntiForgeryToken helper the way MVC does; ViewStateUserKey (see
ViewState and Control State) is the standard
Web-Forms-era CSRF mitigation, binding a page’s ViewState to the authenticated user so a replayed form from a
different user’s session fails MAC-consistent processing. For non-ViewState-carrying endpoints (an .ashx
handler accepting a POST), a hand-rolled anti-CSRF token in a hidden field, validated server-side, is the
equivalent construct.
machineKey and why it matters in a farm
machineKey (introduced in ViewState and Control
State for ViewState signing) also signs and encrypts the Forms authentication ticket and any other protected
data (MachineKeySection-based APIs). In a load-balanced farm, every server must share an explicitly
configured machineKey — auto-generated (the default) keys are per-machine, so a user authenticated by one
server would fail validation on another, and ViewState generated by one server would be rejected by another:
<machineKey validationKey="[64-hex-char shared key]" decryptionKey="[48-hex-char shared key]"
validation="HMACSHA256" decryption="AES" />
Claims-based identity and ASP.NET Identity 2.x on Web Forms
ASP.NET Identity 2.x (the same Microsoft.AspNet.Identity.Core/.Owin packages MVC 5 uses) can be hosted on
Web Forms via OWIN (Microsoft.Owin.Host.SystemWeb), replacing FormsAuthentication/Membership with a
ClaimsPrincipal-based model and EF-backed UserManager/SignInManager:
// Startup.cs (OWIN)
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login")
});
}
}
var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
var user = await userManager.FindByNameAsync(UsernameBox.Text);
bool ok = await userManager.CheckPasswordAsync(user, PasswordBox.Text);
This is the same migration path ASP.NET MVC applications typically take before a full move to ASP.NET Core Identity, and is a reasonable intermediate step for a Web Forms application that needs claims-based identity (e.g. to integrate with an external OAuth/OIDC provider) without a full rewrite. See ASP.NET Security Fundamentals and Introduction to ASP.NET Identity.