Skip to content
This repository was archived by the owner on Jan 24, 2021. It is now read-only.

Commit 292185a

Browse files
committed
Removed use of object serializer for Csrf cookie generation
1 parent d2d0240 commit 292185a

File tree

5 files changed

+158
-51
lines changed

5 files changed

+158
-51
lines changed

src/Nancy.Tests/Unit/Security/CsrfFixture.cs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
using System.Linq;
55
using System.Threading;
66
using FakeItEasy;
7-
87
using Nancy.Bootstrapper;
98
using Nancy.Cryptography;
10-
using Nancy.Helpers;
119
using Nancy.Security;
1210
using Nancy.Tests.Fakes;
1311

@@ -16,28 +14,18 @@
1614
public class CsrfFixture
1715
{
1816
private readonly IPipelines pipelines;
19-
2017
private readonly Request request;
21-
2218
private readonly FakeRequest optionsRequest;
23-
2419
private readonly Response response;
25-
2620
private readonly CryptographyConfiguration cryptographyConfiguration;
2721

28-
private readonly DefaultObjectSerializer objectSerializer;
29-
30-
3122
public CsrfFixture()
3223
{
3324
this.pipelines = new MockPipelines();
3425

3526
this.cryptographyConfiguration = CryptographyConfiguration.Default;
36-
37-
this.objectSerializer = new DefaultObjectSerializer();
3827
var csrfStartup = new CsrfApplicationStartup(
3928
this.cryptographyConfiguration,
40-
this.objectSerializer,
4129
new DefaultCsrfTokenValidator(this.cryptographyConfiguration));
4230

4331
csrfStartup.Initialize(this.pipelines);
@@ -53,30 +41,38 @@ public CsrfFixture()
5341
[Fact]
5442
public void Should_create_cookie_in_response_if_token_exists_in_context()
5543
{
44+
// Given
5645
var context = new NancyContext { Request = this.request, Response = this.response };
5746
context.Items[CsrfToken.DEFAULT_CSRF_KEY] = "TestingToken";
5847

48+
// When
5949
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());
6050

51+
// Then
6152
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
6253
this.response.Cookies.FirstOrDefault(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).Value.ShouldEqual("TestingToken");
6354
}
6455

6556
[Fact]
6657
public void Should_copy_request_cookie_to_context_but_not_response_if_it_exists_and_context_does_not_contain_token()
6758
{
59+
// Given
6860
this.request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, "ValidToken");
6961
var fakeValidator = A.Fake<ICsrfTokenValidator>();
62+
7063
A.CallTo(() => fakeValidator.CookieTokenStillValid(A<CsrfToken>.Ignored)).Returns(true);
64+
7165
var csrfStartup = new CsrfApplicationStartup(
7266
this.cryptographyConfiguration,
73-
this.objectSerializer,
7467
fakeValidator);
68+
7569
csrfStartup.Initialize(this.pipelines);
7670
var context = new NancyContext { Request = this.request, Response = this.response };
7771

72+
// When
7873
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());
7974

75+
// Then
8076
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeFalse();
8177
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
8278
context.Items[CsrfToken.DEFAULT_CSRF_KEY].ShouldEqual("ValidToken");
@@ -85,20 +81,24 @@ public void Should_copy_request_cookie_to_context_but_not_response_if_it_exists_
8581
[Fact]
8682
public void Should_not_generate_a_new_token_on_an_options_request_and_not_add_a_cookie()
8783
{
84+
// Given
8885
this.optionsRequest.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, "ValidToken");
89-
86+
9087
var fakeValidator = A.Fake<ICsrfTokenValidator>();
9188
A.CallTo(() => fakeValidator.CookieTokenStillValid(A<CsrfToken>.Ignored)).Returns(true);
89+
9290
var csrfStartup = new CsrfApplicationStartup(
9391
this.cryptographyConfiguration,
94-
this.objectSerializer,
9592
fakeValidator);
93+
9694
csrfStartup.Initialize(this.pipelines);
9795
var context = new NancyContext { Request = this.optionsRequest, Response = this.response };
9896
context.Items[CsrfToken.DEFAULT_CSRF_KEY] = "ValidToken";
9997

98+
// When
10099
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());
101100

101+
// Then
102102
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeFalse();
103103
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
104104
context.Items[CsrfToken.DEFAULT_CSRF_KEY].ShouldEqual("ValidToken");
@@ -107,18 +107,23 @@ public void Should_not_generate_a_new_token_on_an_options_request_and_not_add_a_
107107
[Fact]
108108
public void Should_regenerage_token_if_invalid()
109109
{
110+
// Given
110111
this.request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, "InvalidToken");
111112
var fakeValidator = A.Fake<ICsrfTokenValidator>();
113+
112114
A.CallTo(() => fakeValidator.CookieTokenStillValid(A<CsrfToken>.Ignored)).Returns(false);
115+
113116
var csrfStartup = new CsrfApplicationStartup(
114117
this.cryptographyConfiguration,
115-
this.objectSerializer,
116118
fakeValidator);
119+
117120
csrfStartup.Initialize(this.pipelines);
118121
var context = new NancyContext { Request = this.request, Response = this.response };
119122

123+
// When
120124
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());
121125

126+
// Then
122127
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
123128
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
124129
context.Items[CsrfToken.DEFAULT_CSRF_KEY].ShouldNotEqual("InvalidToken");
@@ -127,18 +132,22 @@ public void Should_regenerage_token_if_invalid()
127132
[Fact]
128133
public void Should_http_decode_cookie_token_when_copied_to_the_context()
129134
{
135+
// Given
130136
var fakeValidator = A.Fake<ICsrfTokenValidator>();
131137
A.CallTo(() => fakeValidator.CookieTokenStillValid(A<CsrfToken>.Ignored)).Returns(true);
138+
132139
var csrfStartup = new CsrfApplicationStartup(
133140
this.cryptographyConfiguration,
134-
this.objectSerializer,
135141
fakeValidator);
142+
136143
csrfStartup.Initialize(this.pipelines);
137144
this.request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, "Testing Token");
138145
var context = new NancyContext { Request = this.request, Response = this.response };
139146

147+
// When
140148
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());
141149

150+
// Then
142151
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeFalse();
143152
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
144153
context.Items[CsrfToken.DEFAULT_CSRF_KEY].ShouldEqual("Testing Token");
@@ -147,10 +156,13 @@ public void Should_http_decode_cookie_token_when_copied_to_the_context()
147156
[Fact]
148157
public void Should_create_a_new_token_if_one_doesnt_exist_in_request_or_context()
149158
{
159+
// Given
150160
var context = new NancyContext { Request = this.request, Response = this.response };
151161

162+
// When
152163
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());
153164

165+
// Then
154166
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
155167
context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY).ShouldBeTrue();
156168
var cookieValue = this.response.Cookies.FirstOrDefault(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).Value;
@@ -162,12 +174,16 @@ public void Should_create_a_new_token_if_one_doesnt_exist_in_request_or_context(
162174
[Fact]
163175
public void Should_be_able_to_disable_csrf()
164176
{
177+
// Given
165178
var context = new NancyContext { Request = this.request, Response = this.response };
166179
context.Items[CsrfToken.DEFAULT_CSRF_KEY] = "TestingToken";
167180

168181
Csrf.Disable(this.pipelines);
182+
183+
// When
169184
this.pipelines.AfterRequest.Invoke(context, new CancellationToken());
170185

186+
// Then
171187
this.response.Cookies.Any(c => c.Name == CsrfToken.DEFAULT_CSRF_KEY).ShouldBeFalse();
172188
}
173189

@@ -178,7 +194,7 @@ public void ValidateCsrfToken_gets_provided_token_from_form_data()
178194
var token = Csrf.GenerateTokenString();
179195
var context = new NancyContext { Request = this.request };
180196
var module = new FakeNancyModule { Context = context };
181-
197+
182198
// When
183199
context.Request.Form[CsrfToken.DEFAULT_CSRF_KEY] = token;
184200
context.Request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, token);
@@ -194,7 +210,7 @@ public void ValidateCsrfToken_gets_provided_token_from_request_header_if_not_pre
194210
var token = Csrf.GenerateTokenString();
195211
var context = new NancyContext();
196212
var module = new FakeNancyModule { Context = context };
197-
213+
198214
// When
199215
context.Request = RequestWithHeader(CsrfToken.DEFAULT_CSRF_KEY, token);
200216
context.Request.Cookies.Add(CsrfToken.DEFAULT_CSRF_KEY, token);
@@ -208,4 +224,4 @@ private static FakeRequest RequestWithHeader(string header, string value)
208224
return new FakeRequest("GET", "/", new Dictionary<string, IEnumerable<string>> { { header, new[] { value } } });
209225
}
210226
}
211-
}
227+
}

src/Nancy/Security/Csrf.cs

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
namespace Nancy.Security
22
{
33
using System;
4+
using System.Collections.Generic;
5+
using System.Globalization;
46
using System.Linq;
7+
using System.Text;
58

6-
using Cookies;
79
using Nancy.Bootstrapper;
10+
using Nancy.Cookies;
811
using Nancy.Cryptography;
912
using Nancy.Helpers;
1013

@@ -14,12 +17,15 @@
1417
public static class Csrf
1518
{
1619
private const string CsrfHookName = "CsrfPostHook";
20+
private const char ValueDelimiter = '#';
21+
private const char PairDelimiter = '|';
1722

1823
/// <summary>
1924
/// Enables Csrf token generation.
20-
/// This is disabled by default.
2125
/// </summary>
22-
/// <param name="pipelines">Application pipelines</param>
26+
/// <remarks>This is disabled by default.</remarks>
27+
/// <param name="pipelines">The application pipelines.</param>
28+
/// <param name="cryptographyConfiguration">The cryptography configuration. This is <see langword="null" /> by default.</param>
2329
public static void Enable(IPipelines pipelines, CryptographyConfiguration cryptographyConfiguration = null)
2430
{
2531
cryptographyConfiguration = cryptographyConfiguration ?? CsrfApplicationStartup.CryptographyConfiguration;
@@ -35,16 +41,18 @@ public static void Enable(IPipelines pipelines, CryptographyConfiguration crypto
3541

3642
if (context.Items.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY))
3743
{
38-
context.Response.Cookies.Add(new NancyCookie(CsrfToken.DEFAULT_CSRF_KEY,
39-
(string)context.Items[CsrfToken.DEFAULT_CSRF_KEY],
40-
true));
44+
context.Response.Cookies.Add(new NancyCookie(
45+
CsrfToken.DEFAULT_CSRF_KEY,
46+
(string)context.Items[CsrfToken.DEFAULT_CSRF_KEY],
47+
true));
48+
4149
return;
4250
}
4351

4452
if (context.Request.Cookies.ContainsKey(CsrfToken.DEFAULT_CSRF_KEY))
4553
{
4654
var cookieValue = context.Request.Cookies[CsrfToken.DEFAULT_CSRF_KEY];
47-
var cookieToken = CsrfApplicationStartup.ObjectSerializer.Deserialize(cookieValue) as CsrfToken;
55+
var cookieToken = ParseToCsrfToken(cookieValue);
4856

4957
if (CsrfApplicationStartup.TokenValidator.CookieTokenStillValid(cookieToken))
5058
{
@@ -76,7 +84,7 @@ public static void Disable(IPipelines pipelines)
7684
/// Only necessary if a particular route requires a new token for each request.
7785
/// </summary>
7886
/// <param name="module">Nancy module</param>
79-
/// <returns></returns>
87+
/// <param name="cryptographyConfiguration">The cryptography configuration. This is <c>null</c> by default.</param>
8088
public static void CreateNewCsrfToken(this INancyModule module, CryptographyConfiguration cryptographyConfiguration = null)
8189
{
8290
var tokenString = GenerateTokenString(cryptographyConfiguration);
@@ -93,12 +101,21 @@ internal static string GenerateTokenString(CryptographyConfiguration cryptograph
93101
cryptographyConfiguration = cryptographyConfiguration ?? CsrfApplicationStartup.CryptographyConfiguration;
94102
var token = new CsrfToken
95103
{
96-
CreatedDate = DateTime.Now,
104+
CreatedDate = DateTimeOffset.Now
97105
};
106+
98107
token.CreateRandomBytes();
99108
token.CreateHmac(cryptographyConfiguration.HmacProvider);
100-
var tokenString = CsrfApplicationStartup.ObjectSerializer.Serialize(token);
101-
return tokenString;
109+
110+
var builder = new StringBuilder();
111+
112+
builder.AppendFormat("RandomBytes{0}{1}", ValueDelimiter, Convert.ToBase64String(token.RandomBytes));
113+
builder.Append(PairDelimiter);
114+
builder.AppendFormat("Hmac{0}{1}", ValueDelimiter, Convert.ToBase64String(token.Hmac));
115+
builder.Append(PairDelimiter);
116+
builder.AppendFormat("CreatedDate{0}{1}", ValueDelimiter, token.CreatedDate.ToString("o", CultureInfo.InvariantCulture));
117+
118+
return builder.ToString();
102119
}
103120

104121
/// <summary>
@@ -135,7 +152,7 @@ private static CsrfToken GetProvidedToken(Request request)
135152
var providedTokenString = request.Form[CsrfToken.DEFAULT_CSRF_KEY].Value ?? request.Headers[CsrfToken.DEFAULT_CSRF_KEY].FirstOrDefault();
136153
if (providedTokenString != null)
137154
{
138-
providedToken = CsrfApplicationStartup.ObjectSerializer.Deserialize(providedTokenString) as CsrfToken;
155+
providedToken = ParseToCsrfToken(providedTokenString);
139156
}
140157

141158
return providedToken;
@@ -148,10 +165,67 @@ private static CsrfToken GetCookieToken(Request request)
148165
string cookieTokenString;
149166
if (request.Cookies.TryGetValue(CsrfToken.DEFAULT_CSRF_KEY, out cookieTokenString))
150167
{
151-
cookieToken = CsrfApplicationStartup.ObjectSerializer.Deserialize(cookieTokenString) as CsrfToken;
168+
cookieToken = ParseToCsrfToken(cookieTokenString);
152169
}
153170

154171
return cookieToken;
155172
}
173+
174+
private static void AddTokenValue(Dictionary<string, string> dictionary, string key, string value)
175+
{
176+
if (!string.IsNullOrEmpty(key))
177+
{
178+
dictionary.Add(key, value);
179+
}
180+
}
181+
182+
private static CsrfToken ParseToCsrfToken(string cookieTokenString)
183+
{
184+
var parsed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
185+
186+
var currentKey = string.Empty;
187+
var buffer = new StringBuilder();
188+
189+
for (var index = 0; index < cookieTokenString.Length; index++)
190+
{
191+
var currentCharacter = cookieTokenString[index];
192+
193+
switch (currentCharacter)
194+
{
195+
case ValueDelimiter:
196+
currentKey = buffer.ToString();
197+
buffer.Clear();
198+
break;
199+
case PairDelimiter:
200+
AddTokenValue(parsed, currentKey, buffer.ToString());
201+
buffer.Clear();
202+
break;
203+
default:
204+
buffer.Append(currentCharacter);
205+
break;
206+
}
207+
}
208+
209+
AddTokenValue(parsed, currentKey, buffer.ToString());
210+
211+
if (parsed.Keys.Count() != 3)
212+
{
213+
return null;
214+
}
215+
216+
try
217+
{
218+
return new CsrfToken
219+
{
220+
CreatedDate = DateTimeOffset.ParseExact(parsed["CreatedDate"], "o", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
221+
Hmac = Convert.FromBase64String(parsed["Hmac"]),
222+
RandomBytes = Convert.FromBase64String(parsed["RandomBytes"])
223+
};
224+
}
225+
catch
226+
{
227+
return null;
228+
}
229+
}
156230
}
157231
}

0 commit comments

Comments
 (0)