Hi Reaching out to see if anyone has done this before in asp.net forms with Kentico 11-12. We are in the process of moving over to Experience, But we are looking for a quick security fix with not allowing inline-scripts with a nonce policy for CSP (Content-Security-Policty). There is Kentico Code and Custom code that contains Inline-scripts which means the site cannot pass OWASP after having the site vulnerability scanned. I have this working on a standard asp.net forms web project with an html page and it works very well. I Was able to create an IIS Module for Response filtering to update script tags in the html before processing the request back to the user/client. But if I apply this to Kentico CMS 11-12. I Just get a blank web page.
Curious to see if anyone else was able to get a IHttpModule with Kentico or something like this to work with Kentico, It can be done in MVC with the response filters. I have tested in other non-kentico Asp.net web applications and it works, im guessing it has something to do with the Kentico Lifecycle, possible? Below is what I have for code and setup:
In the Appcode folder (CspNonceModule.cs)
using System;
using System.IO;
using System.IO.Compression;
using System.Text.RegularExpressions;
using System.Web;
public class CspNonceModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += OnBeginRequest;
context.PreRequestHandlerExecute += OnPreRequestHandlerExecute;
context.EndRequest += OnEndRequest;
}
private void OnBeginRequest(object sender, EventArgs e)
{
// bad cache
HttpContext.Current.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate";
HttpContext.Current.Response.Headers["Pragma"] = "no-cache";
HttpContext.Current.Response.Headers["Expires"] = "0";
// Generate a unique nonce for this request
string nonce = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
HttpContext.Current.Items["ScriptNonce"] = nonce;
if (nonce != null)
{
// Add Content-Security-Policy header with the nonce
HttpContext.Current.Response.Headers.Add(
"Content-Security-Policy",
$"script-src 'self' 'nonce-{nonce}'"
);
}
}
private void OnPreRequestHandlerExecute(object sender, EventArgs e)
{
// bad cache
HttpContext.Current.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate";
HttpContext.Current.Response.Headers["Pragma"] = "no-cache";
HttpContext.Current.Response.Headers["Expires"] = "0";
HttpContext context = HttpContext.Current;
string contentType = context.Response.ContentType;
string contentEncoding = context.Response.Headers["Content-Encoding"];
System.Diagnostics.Debug.WriteLine($"OnPreRequestHandlerExecute - Content-Type: {contentType}");
System.Diagnostics.Debug.WriteLine($"Content Encoding: {contentEncoding}");
// Apply filter only to HTML content and if encoding is not set
if (contentType?.StartsWith("text/html", StringComparison.OrdinalIgnoreCase) == true && string.IsNullOrEmpty(contentEncoding))
{
HttpContext.Current.Response.ContentType = "text/html";
string nonce = (string)HttpContext.Current.Items["ScriptNonce"];
context.Response.Filter = new ResponseFilterStream(context.Response.Filter, nonce);
}
else
{
System.Diagnostics.Debug.WriteLine($"Skipping non-HTML content: {contentType}");
}
}
private void OnEndRequest(object sender, EventArgs e)
{
string contentType = HttpContext.Current.Response.ContentType;
System.Diagnostics.Debug.WriteLine($"OnEndRequest - Content-Type: {contentType}");
}
public void Dispose()
{
// No resources to dispose
}
}
public class ResponseFilterStream : Stream
{
private readonly Stream _responseStream;
private readonly string _nonce;
private readonly MemoryStream _cacheStream = new MemoryStream();
public ResponseFilterStream(Stream responseStream, string nonce)
{
_responseStream = responseStream;
_nonce = nonce;
}
public override void Write(byte[] buffer, int offset, int count)
{
if (count > 0)
{
string contentEncoding = HttpContext.Current.Response.Headers["Content-Encoding"];
if (contentEncoding == "gzip")
{
DecompressGzipContent(buffer, offset, count);
}
else if (contentEncoding == "deflate")
{
DecompressDeflateContent(buffer, offset, count);
}
else
{
_cacheStream.Write(buffer, offset, count);
}
}
}
private void DecompressGzipContent(byte[] buffer, int offset, int count)
{
System.Diagnostics.Debug.WriteLine("Decompressing GZIP content.");
try
{
using (var gzipStream = new GZipStream(new MemoryStream(buffer, offset, count), CompressionMode.Decompress))
{
gzipStream.CopyTo(_cacheStream);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Decompression failed: {ex.Message}");
_cacheStream.Write(buffer, offset, count); // Fallback to raw data
}
}
private void DecompressDeflateContent(byte[] buffer, int offset, int count)
{
System.Diagnostics.Debug.WriteLine("Decompressing Deflate content.");
try
{
using (var deflateStream = new DeflateStream(new MemoryStream(buffer, offset, count), CompressionMode.Decompress))
{
deflateStream.CopyTo(_cacheStream);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Decompression failed: {ex.Message}");
_cacheStream.Write(buffer, offset, count); // Fallback to raw data
}
}
public override void Flush()
{
if (_cacheStream.Length > 0)
{
_cacheStream.Seek(0, SeekOrigin.Begin);
string html = new StreamReader(_cacheStream).ReadToEnd();
System.Diagnostics.Debug.WriteLine($"Decompressed HTML content: {html.Length} Characters.");
// Modify the HTML to include nonce attributes in script tags
try
{
html = Regex.Replace(html, @"<script\b([^>]*)>", $"<script nonce=\"{_nonce}\"$1>", RegexOptions.IgnoreCase);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error processing HTML: {ex.Message}");
}
// Ensure proper encoding
var encoding = HttpContext.Current.Response.ContentEncoding ?? System.Text.Encoding.UTF8;
byte[] modifiedBuffer = encoding.GetBytes(html);
// Correct Content-Length header after modification
HttpContext.Current.Response.Headers["Content-Length"] = modifiedBuffer.Length.ToString();
// Write modified content to the response stream
_responseStream.Write(modifiedBuffer, 0, modifiedBuffer.Length);
}
// Flush the original response stream to ensure the client gets the modified data
_responseStream.Flush();
}
private void WriteModifiedContent(string html)
{
var encoding = HttpContext.Current.Response.ContentEncoding ?? System.Text.Encoding.UTF8;
byte[] modifiedBuffer = encoding.GetBytes(html);
HttpContext.Current.Response.Headers["Content-Length"] = modifiedBuffer.Length.ToString();
//HttpContext.Current.Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate";
//HttpContext.Current.Response.Headers["Pragma"] = "no-cache";
//HttpContext.Current.Response.Headers["Expires"] = "0";
_responseStream.Write(modifiedBuffer, 0, modifiedBuffer.Length);
}
// Implement required Stream members
public override bool CanRead => _responseStream.CanRead;
public override bool CanSeek => _responseStream.CanSeek;
public override bool CanWrite => _responseStream.CanWrite;
public override long Length => _responseStream.Length;
public override long Position
{
get { return _responseStream.Position; }
set { _responseStream.Position = value; }
}
public override void SetLength(long value) => _responseStream.SetLength(value);
public override long Seek(long offset, SeekOrigin origin) => _responseStream.Seek(offset, origin);
public override int Read(byte[] buffer, int offset, int count) => _responseStream.Read(buffer, offset, count);
}
Then in the web.config you have to reference the module with the rest of the site modules
<system.webServer>
<httpProtocol>
<modules>
<remove name="WebDAVModule" />
<remove name="XHtmlModule" />
<remove name="CMSApplicationModule" />
<add name="XHtmlModule" type="CMS.OutputFilter.OutputFilterModule, CMS.OutputFilter" />
<add name="CMSApplicationModule" preCondition="managedHandler" type="CMS.Base.ApplicationModule, CMS.Base" />
<add name="CspNonceModule" type="CspNonceModule" />
</modules>
</httpProtocol>
</system.webServer>
Thanks!, Much appreciated