.Net Core Penetration Testing

When performing a penetration test on a .Net Core web site to find security vulnerabilities, some common issues may be found that are not handled by the default .Net Core template in Visual Studio. One of the tools I use to carry out penetration tests in the Zed Attack Proxy (ZAP) that is part of Open Web Application Security Project (OWASP).

Below is a list of some of the common alerts that may be flagged by ZAP or similar tools, and how to fix each one in .Net Core. All fixes are done within the Configure() method of the Startup class. At the end I will also include a code listing of all the fixes together.

X-Frame-Options Header Not Set

Risk: Medium

ZAP description: X-Frame-Options header is not included in the HTTP response to protect against ‘ClickJacking’ attacks.

Reference: http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx

Fix:

app.Use(async (context, next) =>
{
	context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
	await next();
});

Cookie No HttpOnly Flag

Risk: Low

ZAP description: A cookie has been set without the HttpOnly flag, which means that the cookie can be accessed by JavaScript. If a malicious script can be run on this page then the cookie will be accessible and can be transmitted to another site. If this is a session cookie then session hijacking may be possible.

Reference: http://www.owasp.org/index.php/HttpOnly

Fix:

app.UseCookiePolicy(new CookiePolicyOptions
{
	HttpOnly = HttpOnlyPolicy.Always
});

Cookie Without Secure Flag

Risk: Low

ZAP description: A cookie has been set without the secure flag, which means that the cookie can be accessed via unencrypted connections.

Reference: http://www.owasp.org/index.php/Testing_for_cookies_attributes_(OWASP-SM-002)

Fix:

app.UseCookiePolicy(new CookiePolicyOptions
{
	Secure = CookieSecurePolicy.Always
});

Incomplete or No Cache-control and Pragma HTTP Header Set

Risk: Low

ZAP description: The cache-control and pragma HTTP header have not been set properly or are missing allowing the browser and proxies to cache content.

Reference: https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Web_Content_Caching

Fix:

app.UseStaticFiles(new StaticFileOptions
{
	OnPrepareResponse = ctx =>
	{
		ctx.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
	}
});

app.UseMiddleware<NoCacheMiddleware>();

Add a new Middleware Class:

public class NoCacheMiddleware
{
	private readonly RequestDelegate m_next;
	public NoCacheMiddleware(RequestDelegate next)
	{
		m_next = next;
	}
	public async Task Invoke(HttpContext httpContext)
	{
		httpContext.Response.OnStarting((state) =>
		{
			// ref: <a href="http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-cached-across-all-browsers">http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-cached-across-all-browsers</a>
			httpContext.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
			httpContext.Response.Headers.Append("Pragma", "no-cache");
			httpContext.Response.Headers.Append("Expires", "0");
			return Task.FromResult(0);
		}, null);
		await m_next.Invoke(httpContext);
	}
}

Web Browser XSS Protection Not Enabled

Risk: Low

ZAP description: Web Browser XSS Protection is not enabled, or is disabled by the configuration of the ‘X-XSS-Protection’ HTTP response header on the web server

Reference: https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
https://blog.veracode.com/2014/03/guidelines-for-setting-security-headers/

Fix:

app.Use(async (context, next) =>
{
	context.Response.Headers.Add("X-Xss-Protection", "1");
	await next();
});

X-Content-Type-Options Header Missing

Risk: Low

ZAP description: The Anti-MIME-Sniffing header X-Content-Type-Options was not set to ‘nosniff’. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.

Reference: http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx
https://www.owasp.org/index.php/List_of_useful_HTTP_headers

Fix:

app.UseStaticFiles(new StaticFileOptions
{
	OnPrepareResponse = ctx =>
	{
		ctx.Context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
	}
});

app.Use(async (context, next) =>
{
	context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
	await next();
});

Complete listing

Below is a 'complete listing' combining the individual fixes. I have removed some of the unrelated code to help with readability.

Startup class

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	app.UseResponseCompression();
	loggerFactory.AddLog4Net();
	
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
		app.UseDatabaseErrorPage();
	}
	else
	{
		app.UseExceptionHandler("/Home/Error");
	}
	
	app.UseStaticFiles(new StaticFileOptions
	{
		//Pen test fix
		OnPrepareResponse = ctx =>
		{
			ctx.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
			ctx.Context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
		}
	});
	app.UseAuthentication();
	app.UseSession();

	//Pen test fix
	app.UseCookiePolicy(new CookiePolicyOptions
	{
		HttpOnly = HttpOnlyPolicy.Always,
		Secure = CookieSecurePolicy.Always
	});

	//Pen test fix
	app.Use(async (context, next) =>
	{
		context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
		context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
		context.Response.Headers.Add("X-Xss-Protection", "1");
		await next();
	});

	//Pen test fix
	app.UseMiddleware<NoCacheMiddleware>();

	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller=Home}/{action=Index}/{id?}");
	});
}

Middleware Class:

public class NoCacheMiddleware
{
	private readonly RequestDelegate m_next;
	public NoCacheMiddleware(RequestDelegate next)
	{
		m_next = next;
	}
	public async Task Invoke(HttpContext httpContext)
	{
		httpContext.Response.OnStarting((state) =>
		{
			// ref: <a href="http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-cached-across-all-browsers">http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-cached-across-all-browsers</a>
			httpContext.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
			httpContext.Response.Headers.Append("Pragma", "no-cache");
			httpContext.Response.Headers.Append("Expires", "0");
			return Task.FromResult(0);
		}, null);
		await m_next.Invoke(httpContext);
	}
}

Extra: X-Powered-By and Server Headers

The following headers are added by Azure to each request:
  • X-Powered-By: ASP.NET
  • Server: Kestrel
If you want to remove these header's to make it harder for a potential attacked to know what server is your app is running on, then add the following to your web.config file:
<system.webServer>
	<!--<span style="color:#008000;font-family:Consolas;"> Additional entries removed to help readability-->
	<security>
		<requestFiltering removeServerHeader ="true"></requestFiltering>
	</security>
	<httpProtocol>
		<customHeaders>
			<remove name="X-Powered-By"/>
		</customHeaders>
	</httpProtocol>
</system.webServer></blockquote>
Alex Orpwood Written by:

Software developing and architecting for 20 years. Satellite monitoring by day, writing my own app by night. More about me