From 14cbaabd5479b1216b050fa556973a490b4ed403 Mon Sep 17 00:00:00 2001 From: Pieter Germishuys Date: Mon, 8 Dec 2025 09:08:35 +0100 Subject: [PATCH 1/2] Resolve CSP by moving it to a not dotnet formattable file - Introduce CSP Header Validation Tests --- .../Duende.IdentityServer.csproj | 5 + .../Endpoints/Results/CheckSessionResult.cs | 318 ++---------------- .../Endpoints/Results/check-session-result.js | 296 ++++++++++++++++ .../IdentityServer/IdentityServerConstants.cs | 8 +- .../Endpoints/Results/AuthorizeResultTests.cs | 27 ++ .../Results/CheckSessionResultTests.cs | 20 ++ .../Results/EndSessionCallbackResultTests.cs | 24 ++ 7 files changed, 395 insertions(+), 303 deletions(-) create mode 100644 identity-server/src/IdentityServer/Endpoints/Results/check-session-result.js diff --git a/identity-server/src/IdentityServer/Duende.IdentityServer.csproj b/identity-server/src/IdentityServer/Duende.IdentityServer.csproj index 1ad18b0b6..ff63ff432 100644 --- a/identity-server/src/IdentityServer/Duende.IdentityServer.csproj +++ b/identity-server/src/IdentityServer/Duende.IdentityServer.csproj @@ -28,4 +28,9 @@ + + + + + diff --git a/identity-server/src/IdentityServer/Endpoints/Results/CheckSessionResult.cs b/identity-server/src/IdentityServer/Endpoints/Results/CheckSessionResult.cs index 4cb93c271..4387775de 100644 --- a/identity-server/src/IdentityServer/Endpoints/Results/CheckSessionResult.cs +++ b/identity-server/src/IdentityServer/Endpoints/Results/CheckSessionResult.cs @@ -21,6 +21,7 @@ internal class CheckSessionHttpWriter : IHttpResponseWriter { public CheckSessionHttpWriter(IdentityServerOptions options) => _options = options; + private static readonly string CheckSessionScript = GetEmbeddedResource($"{typeof(CheckSessionHttpWriter).Namespace}.check-session-result.js"); private IdentityServerOptions _options; private static volatile string FormattedHtml; private static readonly object Lock = new object(); @@ -43,7 +44,10 @@ internal class CheckSessionHttpWriter : IHttpResponseWriter { if (cookieName != LastCheckSessionCookieName) { - FormattedHtml = Html.Replace("{cookieName}", cookieName); + FormattedHtml = Html.Replace("{cookieName}", cookieName) + .Replace("{script}", CheckSessionScript, StringComparison.InvariantCulture) + .ReplaceLineEndings("\n"); + LastCheckSessionCookieName = cookieName; } } @@ -51,6 +55,19 @@ internal class CheckSessionHttpWriter : IHttpResponseWriter return FormattedHtml; } + private static string GetEmbeddedResource(string resourceName) + { + var assembly = typeof(CheckSessionHttpWriter).Assembly; + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + throw new InvalidOperationException($"Embedded resource '{resourceName}' not found."); + } + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + private const string Html = @" @@ -62,304 +79,7 @@ internal class CheckSessionHttpWriter : IHttpResponseWriter - + "; diff --git a/identity-server/src/IdentityServer/Endpoints/Results/check-session-result.js b/identity-server/src/IdentityServer/Endpoints/Results/check-session-result.js new file mode 100644 index 000000000..3a6336dfc --- /dev/null +++ b/identity-server/src/IdentityServer/Endpoints/Results/check-session-result.js @@ -0,0 +1,296 @@ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +/* SHA-256 implementation in JavaScript (c) Chris Veness 2002-2014 / MIT Licence */ +/* */ +/* - see http://csrc.nist.gov/groups/ST/toolkit/secure_hashing.html */ +/* http://csrc.nist.gov/groups/ST/toolkit/examples.html */ +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +/* jshint node:true *//* global define, escape, unescape */ +'use strict'; + + +/** + * SHA-256 hash function reference implementation. + * + * @namespace + */ +var Sha256 = {}; + + +/** + * Generates SHA-256 hash of string. + * + * @param {string} msg - String to be hashed + * @returns {string} Hash of msg as hex character string + */ +Sha256.hash = function(msg) { + // convert string to UTF-8, as SHA only deals with byte-streams + msg = msg.utf8Encode(); + + // constants [§4.2.2] + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ]; + // initial hash value [§5.3.1] + var H = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ]; + + // PREPROCESSING + + msg += String.fromCharCode(0x80); // add trailing '1' bit (+ 0's padding) to string [§5.1.1] + + // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1] + var l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length + var N = Math.ceil(l/16); // number of 16-integer-blocks required to hold 'l' ints + var M = new Array(N); + + for (var i=0; i>> 32, but since JS converts + // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators + M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14]); + M[N-1][15] = ((msg.length-1)*8) & 0xffffffff; + + + // HASH COMPUTATION [§6.1.2] + + var W = new Array(64); var a, b, c, d, e, f, g, h; + for (var i=0; i>> n) | (x << (32-n)); +}; + +/** + * Logical functions [§4.1.2]. + * @private + */ +Sha256.Σ0 = function(x) { return Sha256.ROTR(2, x) ^ Sha256.ROTR(13, x) ^ Sha256.ROTR(22, x); }; +Sha256.Σ1 = function(x) { return Sha256.ROTR(6, x) ^ Sha256.ROTR(11, x) ^ Sha256.ROTR(25, x); }; +Sha256.σ0 = function(x) { return Sha256.ROTR(7, x) ^ Sha256.ROTR(18, x) ^ (x>>>3); }; +Sha256.σ1 = function(x) { return Sha256.ROTR(17, x) ^ Sha256.ROTR(19, x) ^ (x>>>10); }; +Sha256.Ch = function(x, y, z) { return (x & y) ^ (~x & z); }; +Sha256.Maj = function(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); }; + + +/** + * Hexadecimal representation of a number. + * @private + */ +Sha256.toHexStr = function(n) { + // note can't use toString(16) as it is implementation-dependant, + // and in IE returns signed numbers when used on full words + var s='', v; + for (var i=7; i>=0; i--) { v = (n>>>(i*4)) & 0xf; s += v.toString(16); } + return s; +}; + + +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + + +/** Extend String object with method to encode multi-byte string to utf8 + * - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html */ +if (typeof String.prototype.utf8Encode == 'undefined') { + String.prototype.utf8Encode = function() { + return unescape( encodeURIComponent( this ) ); + }; +} + +/** Extend String object with method to decode utf8 string to multi-byte */ +if (typeof String.prototype.utf8Decode == 'undefined') { + String.prototype.utf8Decode = function() { + try { + return decodeURIComponent( escape( this ) ); + } catch (e) { + return this; // invalid UTF-8? return as-is + } + }; +} + + +/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ +if (typeof module != 'undefined' && module.exports) module.exports = Sha256; // CommonJs export +if (typeof define == 'function' && define.amd) define([], function() { return Sha256; }); // AMD + +//////////////////////////////////////////////////////////////////// +///////////// IdentityServer JS Code Starts here /////////////////// +//////////////////////////////////////////////////////////////////// + +function getCookies() { + var allCookies = document.cookie; + var cookies = allCookies.split(';'); + return cookies.map(function(value) { + var parts = value.trim().split('='); + if (parts.length === 2) { + return { + name: parts[0].trim(), + value: parts[1].trim() + }; + } + }).filter(function(item) { + return item && item.name && item.value; + }); +} + +function getBrowserSessionId() { + var cookies = getCookies().filter(function(cookie) { + return (cookie.name === cookieName); + }); + // empty string represents anonymous sid + return (cookies[0] && cookies[0].value) || ''; +} + +/*! (c) Tom Wu | http://www-cs-students.stanford.edu/~tjw/jsbn/ */ +var b64map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +var b64pad = '='; + +function hex2b64(h) { + var i; + var c; + var ret = ''; + for (i = 0; i + 3 <= h.length; i += 3) { + c = parseInt(h.substring(i, i + 3), 16); + ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63); + } + if (i + 1 == h.length) { + c = parseInt(h.substring(i, i + 1), 16); + ret += b64map.charAt(c << 2); + } + else if (i + 2 == h.length) { + c = parseInt(h.substring(i, i + 2), 16); + ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4); + } + if (b64pad) while ((ret.length & 3) > 0) ret += b64pad; + return ret; +} + +function base64UrlEncode(s){ + var val = hex2b64(s); + + val = val.replace(/=/g, ''); // Remove any trailing '='s + val = val.replace(/\+/g, '-'); // '+' => '-' + val = val.replace(/\//g, '_'); // '/' => '_' + + return val; +} + +function hash(value) { + var hash = Sha256.hash(value); + return base64UrlEncode(hash); +} + +function computeSessionStateHash(clientId, origin, sessionId, salt) { + return hash(clientId + origin + sessionId + salt); +} + +function calculateSessionStateResult(origin, message) { + try { + if (!origin || !message) { + return 'error'; + } + + var idx = message.lastIndexOf(' '); + if (idx < 0 || idx >= message.length) { + return 'error'; + } + + var clientId = message.substring(0, idx); + var sessionState = message.substring(idx + 1); + + if (!clientId || !sessionState) { + return 'error'; + } + + var sessionStateParts = sessionState.split('.'); + if (sessionStateParts.length !== 2) { + return 'error'; + } + + var clientHash = sessionStateParts[0]; + var salt = sessionStateParts[1]; + if (!clientHash || !salt) { + return 'error'; + } + + var currentSessionId = getBrowserSessionId(); + var expectedHash = computeSessionStateHash(clientId, origin, currentSessionId, salt); + return clientHash === expectedHash ? 'unchanged' : 'changed'; + } + catch (e) { + return 'error'; + } +} + +var cookieNameElem = document.getElementById('cookie-name'); +if (cookieNameElem) { + var cookieName = cookieNameElem.textContent.trim(); +} + +if (cookieName && window.parent !== window) { + window.addEventListener('message', function(e) { + if (window === e.source) { + // ignore browser extensions that are sending messages. + return; + } + + if (typeof e.data !== 'string') { + return; + } + + var result = calculateSessionStateResult(e.origin, e.data); + e.source.postMessage(result, e.origin); + }, false); +} diff --git a/identity-server/src/IdentityServer/IdentityServerConstants.cs b/identity-server/src/IdentityServer/IdentityServerConstants.cs index e41d3f6b0..fad6ad721 100644 --- a/identity-server/src/IdentityServer/IdentityServerConstants.cs +++ b/identity-server/src/IdentityServer/IdentityServerConstants.cs @@ -221,19 +221,19 @@ public static class IdentityServerConstants public static class ContentSecurityPolicyHashes { /// - /// The hash of the inline style used on the end session endpoint. + /// The hash of the inline style used on the end session endpoint. /// public const string EndSessionStyle = "sha256-e6FQZewefmod2S/5T11pTXjzE2vn3/8GRwWOs917YE4="; /// - /// The hash of the inline script used on the authorize endpoint. + /// The hash of the inline script used on the authorize endpoint. /// public const string AuthorizeScript = "sha256-orD0/VhH8hLqrLxKHD/HUEMdwqX6/0ve7c5hspX5VJ8="; /// - /// The hash of the inline script used on the check session endpoint. + /// The hash of the inline script used on the check session endpoint. /// - public const string CheckSessionScript = "sha256-fa5rxHhZ799izGRP38+h4ud5QXNT0SFaFlh4eqDumBI="; + public const string CheckSessionScript = "sha256-jyguj/c+mxOUX7TJrFnIkEQlj4jinO1nejo8qnuF1jc="; } public static class ProtocolRoutePaths diff --git a/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/AuthorizeResultTests.cs b/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/AuthorizeResultTests.cs index a6cca4216..fe3057d33 100644 --- a/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/AuthorizeResultTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/AuthorizeResultTests.cs @@ -2,6 +2,7 @@ // See LICENSE in the project root for license information. +using System.Text.RegularExpressions; using Duende.IdentityModel; using Duende.IdentityServer; using Duende.IdentityServer.Configuration; @@ -221,6 +222,32 @@ public class AuthorizeResultTests html.ShouldContain(""); } + [Fact] + public async Task csp_hash_should_match_inline_script() + { + _response.Request = new ValidatedAuthorizeRequest + { + ClientId = "client", + ResponseMode = OidcConstants.ResponseModes.FormPost, + RedirectUri = "http://client/callback", + State = "state" + }; + + await _subject.WriteHttpResponse(new AuthorizeResult(_response), _context); + + _context.Response.StatusCode.ShouldBe(200); + _context.Response.ContentType.ShouldStartWith("text/html"); + _context.Response.Body.Seek(0, SeekOrigin.Begin); + using var rdr = new StreamReader(_context.Response.Body); + var html = await rdr.ReadToEndAsync(); + + var match = Regex.Match(html, "", RegexOptions.Singleline | RegexOptions.IgnoreCase); + match.Success.ShouldBeTrue(); + + var scriptSha256 = "sha256-" + match.Groups[1].Value.ToSha256(); + IdentityServerConstants.ContentSecurityPolicyHashes.AuthorizeScript.ShouldContain(scriptSha256); + } + [Fact] public async Task form_post_mode_should_add_unsafe_inline_for_csp_level_1() { diff --git a/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/CheckSessionResultTests.cs b/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/CheckSessionResultTests.cs index 0d17795b6..975e6a707 100644 --- a/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/CheckSessionResultTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/CheckSessionResultTests.cs @@ -2,6 +2,8 @@ // See LICENSE in the project root for license information. +using System.Text.RegularExpressions; +using Duende.IdentityModel; using Duende.IdentityServer; using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Endpoints.Results; @@ -46,6 +48,24 @@ public class CheckSessionResultTests html.ShouldContain(""); } + [Fact] + public async Task csp_hash_should_match_inline_script() + { + await _subject.WriteHttpResponse(new CheckSessionResult(), _context); + + _context.Response.StatusCode.ShouldBe(200); + _context.Response.ContentType.ShouldStartWith("text/html"); + _context.Response.Body.Seek(0, SeekOrigin.Begin); + using var rdr = new StreamReader(_context.Response.Body); + var html = await rdr.ReadToEndAsync(); + + var match = Regex.Match(html, "", RegexOptions.Singleline | RegexOptions.IgnoreCase); + match.Success.ShouldBeTrue(); + + var scriptSha256 = "sha256-" + match.Groups[1].Value.ToSha256(); + IdentityServerConstants.ContentSecurityPolicyHashes.CheckSessionScript.ShouldContain(scriptSha256); + } + [Fact] public async Task form_post_mode_should_add_unsafe_inline_for_csp_level_1() { diff --git a/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/EndSessionCallbackResultTests.cs b/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/EndSessionCallbackResultTests.cs index 74eb6204f..48eb5869b 100644 --- a/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/EndSessionCallbackResultTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/EndSessionCallbackResultTests.cs @@ -2,6 +2,9 @@ // See LICENSE in the project root for license information. +using System.Text.RegularExpressions; +using Duende.IdentityModel; +using Duende.IdentityServer; using Duende.IdentityServer.Configuration; using Duende.IdentityServer.Endpoints.Results; using Duende.IdentityServer.Models; @@ -64,6 +67,27 @@ public class EndSessionCallbackResultTests html.ShouldContain(""); } + [Fact] + public async Task csp_hash_should_match_inline_style() + { + _result.IsError = false; + _result.FrontChannelLogoutUrls = new string[] { "http://foo.com", "http://bar.com" }; + + await _subject.WriteHttpResponse(new EndSessionCallbackResult(_result), _context); + + _context.Response.StatusCode.ShouldBe(200); + _context.Response.ContentType.ShouldStartWith("text/html"); + _context.Response.Body.Seek(0, SeekOrigin.Begin); + using var rdr = new StreamReader(_context.Response.Body); + var html = await rdr.ReadToEndAsync(); + + var match = Regex.Match(html, "", RegexOptions.Singleline | RegexOptions.IgnoreCase); + match.Success.ShouldBeTrue(); + + var styleSha256 = "sha256-" + match.Groups[1].Value.ToSha256(); + IdentityServerConstants.ContentSecurityPolicyHashes.EndSessionStyle.ShouldContain(styleSha256); + } + [Fact] public async Task fsuccess_should_add_unsafe_inline_for_csp_level_1() { From c717d1fcd1aa4ae8d936bfbda9b630766b77dd7c Mon Sep 17 00:00:00 2001 From: Brett Hazen <2651260+bhazen@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:49:32 -0600 Subject: [PATCH 2/2] Fixed issuee w/EF tests caused by change in .net --- Directory.Packages.props | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 92d60048b..f27ace590 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -34,8 +34,8 @@ - - + + @@ -43,16 +43,16 @@ - - - - - - - - - - + + + + + + + + + + @@ -134,9 +134,9 @@ that future versions of the intermediate dependencies that don't have this problem exist someday). --> - - + + - \ No newline at end of file +