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

MembershipProvider

SqlMembershipProvider (ASPNETDB schema), ActiveDirectoryMembershipProvider

RoleProvider

SqlRoleProvider, AuthorizationStoreRoleProvider (Authorization Manager/.xml/AD store)

ProfileProvider

SqlProfileProvider

<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

Login

Username/password form; Authenticate event for custom validation logic.

LoginView

Shows different markup templates for anonymous vs. authenticated (optionally per-role) users.

LoginStatus

A "Log in"/"Log out" link that swaps automatically based on auth state.

CreateUserWizard

Self-service registration against the configured MembershipProvider.

PasswordRecovery

Email-based password reset flow (requires MembershipProvider.EnablePasswordRetrieval or reset support and a configured SMTP <mailSettings>).

ChangePassword

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.