One of the most frequent vulnerabilities discovered in web applications is cross-site scripting (XSS). It happens when a victim's browser executes malicious scripts that have been injected into online sites or APIs, possibly compromising sensitive data. This post will describe how to use security best practices, sanitize inputs, and encode outputs to prevent XSS attacks in an ASP.NET Core Web API.

XSS: What is it?
When harmful scripts are injected into content that is given to users without the necessary validation or encoding, it is known as an XSS attack. When it comes to Web APIs, the fact that the API can process user input and then provide it to clients (browsers or other consumers) raises the possibility of criminal activity and script execution.

Example of an XSS Attack
Imagine a Web API that accepts user input, such as a name, and returns it back to the client.

{
    "name": "<script>alert('XSS Attack!');</script>"
}


If the API does not sanitize this input, the malicious JavaScript (<script>alert('XSS Attack!');</script>) will be executed in the client’s browser.

Types of XSS Attacks

  • Stored XSS: Malicious scripts are stored in the database or file system and executed when the victim visits the page that retrieves and displays the data.
  • Reflected XSS: Malicious scripts are embedded in the URL and executed when the victim clicks the link.
  • DOM-Based XSS: The vulnerability is within the client-side JavaScript itself.

Preventing XSS in ASP.NET Core Web API
Here’s a step-by-step guide to protect your API from XSS attacks.

1. Input Validation and Data Annotations

The first line of defense against XSS is to validate user inputs using model validation and constraints.
In ASP.NET Core, you can use Data Annotations to specify validation rules for models. For example, a user registration API might look like this:
public class UserInput
{
    [Required]
    [MaxLength(50)]
    [RegularExpression(@"^[a-zA-Z0-9]*$", ErrorMessage = "Invalid characters in name")]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }
}


By applying these annotations, the API ensures that only valid data is accepted and limits the possibility of malicious scripts getting through.

Example API Controller
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    [HttpPost]
    [Route("create")]
    public IActionResult CreateUser([FromBody] UserInput userInput)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        return Ok(new { Message = "User created successfully!" });
    }
}


In the above example, the input Name is restricted to alphanumeric characters, which limits the possibility of script injection.

2. Sanitizing User Input

Even with validation in place, you should sanitize inputs that could potentially be harmful. ASP.NET Core provides various ways to sanitize and encode user inputs.
Using the HtmlEncoder Class: You can use the HtmlEncoder class to encode dangerous characters before processing the input.
    using System.Text.Encodings.Web;
    public string SanitizeInput(string input)
    {
        return HtmlEncoder.Default.Encode(input);
    }


This encodes any special characters like <, >, or & that could be used for XSS attacks.

Example Usage in API
[HttpPost]
[Route("sanitize")]
public IActionResult SanitizeUserInput([FromBody] string userInput)
{
    var sanitizedInput = HtmlEncoder.Default.Encode(userInput);
    return Ok(new { SanitizedInput = sanitizedInput });
}


3. Using a Third-Party Library for Sanitization
For more complex scenarios, you can use a third-party library like Ganss.XSS, which allows for advanced HTML sanitization.
Install the NuGet Package
Install-Package Ganss.XSS


Example Code
using Ganss.XSS;
public string SanitizeHtml(string input)
{
    var sanitizer = new HtmlSanitizer();
    return sanitizer.Sanitize(input);
}


4. Content Security Policy (CSP)
A Content Security Policy (CSP) is a security header that helps prevent XSS by controlling which resources (scripts, images, styles) can be loaded by the browser.
You can add CSP headers in ASP.NET Core like this.

public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self'");
        await next();
    });

}


This policy restricts the loading of scripts to only those from the same domain, making it much harder for attackers to load malicious external scripts.

5. HTTP-Only and Secure Cookies

If your Web API works with cookies (e.g., for authentication), always mark them as HttpOnly and Secure to prevent client-side scripts from accessing them.
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;


6. Sanitize Data from Third-Party APIs
If your API consumes data from third-party services, it's important to sanitize that data before returning it to your clients. Even trusted APIs could be compromised, so always validate and sanitize the data.

Example
public IActionResult FetchDataFromExternalApi()
{
    var externalApiData = GetExternalApiData();
    var sanitizedData = HtmlEncoder.Default.Encode(externalApiData);
    return Ok(new { Data = sanitizedData });
}


7. Ensure Proper Response Headers
Return the proper Content-Type headers in your API responses. If you’re returning JSON, ensure the Content-Type is set to application/json. This prevents browsers from interpreting JSON responses as HTML or scripts.
context.Response.ContentType = "application/json";

8. Real-World Example of XSS Prevention
Let’s implement an API endpoint that accepts a user's comment and sanitizes the input before storing it in the database.
    Comment Model
    public class Comment
    {
        public int Id { get; set; }
        [Required]
        [MaxLength(500)]
        public string Content { get; set; }
        public DateTime CreatedAt { get; set; }
    }


CommentController
[ApiController]
[Route("api/[controller]")]
public class CommentController : ControllerBase
{
    private readonly HtmlSanitizer _sanitizer;
    public CommentController()
    {
        _sanitizer = new HtmlSanitizer();
    }
    [HttpPost]
    [Route("create")]
    public IActionResult CreateComment([FromBody] Comment comment)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        comment.Content = _sanitizer.Sanitize(comment.Content);
        comment.CreatedAt = DateTime.UtcNow;
        _dbContext.Comments.Add(comment);
        _dbContext.SaveChanges();
        return Ok(new { Message = "Comment created successfully!" });
    }
}


Complete End to end Example In Asp.net Core Web API
Step 1. Create a New ASP.NET Core Web API Project

  • Open Visual Studio Code.
  • Create a new folder for your project.
  • Open the folder in VS Code.
  • Open a terminal in VS Code and run the following command to create an ASP.NET Core Web API project.

dotnet new webapi -n XSSAttacksinASP.NETCoreWebAPI

Step 2. Add Ganss.XSS NuGet Package
Navigate to the project folder and install the Ganss.XSS NuGet package, which will help with input sanitization.
dotnet add package Ganss.XSS

Step 3. Modify the Project Structure
Our project structure should look something like this.

XSSAttacksinASP.NETCoreWebAPI/

├── Controllers/
│   └── CommentController.cs

├── Models/
│   └── Comment.cs

├── Program.cs
├── Startup.cs
├── XssPreventionApi.csproj
└── appsettings.json


Step 4. Define the Model (Comment.cs)
In the Models folder, create a Comment.cs file to define the model for user input.

using System.ComponentModel.DataAnnotations;
namespace XSSAttacksinASP.NETCoreWebAPI.Models
{
    public class Comment
    {
        public int Id { get; set; }
        [Required]
        [MaxLength(500, ErrorMessage = "Comment cannot be longer than 500 characters.")]
        public string Content { get; set; }
        public DateTime CreatedAt { get; set; }
    }
}


Step 5. Create the Comment Controller (CommentController.cs)
In the Controllers folder, create a CommentController.cs file to handle incoming HTTP requests for the comments. We'll use the Ganss.XSS library to sanitize the comment content before saving it to a data store (in this example, we'll just simulate saving it).
using Ganss.Xss;
using Microsoft.AspNetCore.Mvc;
using XSSAttacksinASP.NETCoreWebAPI.Models;
namespace XSSAttacksinASP.NETCoreWebAPI.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class CommentController : ControllerBase
    {
        private readonly HtmlSanitizer _sanitizer;
        public CommentController()
        {
            _sanitizer = new HtmlSanitizer();
        }
        [HttpPost]
        [Route("create")]
        public IActionResult CreateComment([FromBody] Comment comment)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            comment.Content = _sanitizer.Sanitize(comment.Content);
            comment.CreatedAt = DateTime.UtcNow;
            return Ok(new { Message = "Comment created successfully!", SanitizedContent = comment.Content });
        }
    }
}


Step 6. Configure Program. cs and Startup. cs (ASP.NET Core 6+)
Since ASP.NET Core 6+ has consolidated the Startup and Program files, you may just need to ensure the basic structure is in place to handle API routing and services.
namespace XSSAttacksinASP.NETCoreWebAPI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            // Add services to the container.
            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();
            var app = builder.Build();
            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }
            app.UseHttpsRedirection();
            app.UseAuthorization();
            app.MapControllers();
            app.Run();
        }
    }
}


Step 7. Add Launch Settings (Optional)
If you want to configure launch settings (for example, to run the project on a specific port), modify the Properties/launchSettings.json file.
{
  "profiles": {
    "XssPreventionApi": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Step 8. Run the Project
Open the terminal and navigate to the project folder (XssPreventionApi).

Run the project using the following command.
dotnet run

Step 9. Test the API Using Postman or curl
You can use Postman or Curl to send POST requests to the API.

    Example Request with curl
    curl -X POST https://localhost:5001/api/comment/create \
    -H "Content-Type: application/json" \
    -d "{\"Content\": \"<script>alert('XSS Attack!');</script>\"}"


Example Response
{
  "message": "Comment created successfully!",
  "sanitizedContent": "\"}"
}


In the response, you'll see that the <script> tags have been sanitized, preventing the malicious code from executing.

Step 10. Testing XSS Prevention
You can test by sending different kinds of potentially harmful input, such as.
    <script>alert('XSS')</script>
    <img src="x" onerror="alert('XSS')"/>
    onmouseover="alert('XSS')"

All these will be sanitized to their encoded form to ensure no harmful JavaScript gets executed.