1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
|
//------------------------------------------------------------------------------
// <copyright file="FormsAuthenticationTicketSerializer.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Web.Security {
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Web.Util;
// A helper class which can serialize / deserialize FormsAuthenticationTicket instances.
//
// MSRC 11838 / DevDiv #292994 (http://vstfdevdiv:8080/DevDiv2/web/wi.aspx?id=292994):
// We need to fix the format of the serialized FormsAuthenticationTicket to account for
// the fact that the string payloads can contain any arbitrary characters, including
// embedded nulls. In particular, because of that vulnerability, we must assume that *any*
// FormsAuthenticationTicket generated by a pre-patch system is potentially the result
// of a malicious action. This new serialized format was chosen because it guarantees
// a compatibility break between either old format and the new format: pre-patch systems
// will reject post-patch tickets as having an invalid format, and post-patch systems
// will also reject pre-patch tickets as having an invalid format.
/* Current (v1) ticket format
* ==========================
*
* Serialized ticket format version number: 1 byte
* FormsAuthenticationTicket.Version: 1 byte
* FormsAuthenticationTicket.IssueDateUtc: 8 bytes
* {spacer}: 1 byte
* FormsAuthenticationTicket.ExpirationUtc: 8 bytes
* FormsAuthenticationTicket.IsPersistent: 1 byte
* FormsAuthenticationTicket.Name: 1+ bytes (1+ length prefix, 0+ payload)
* FormsAuthenticationTicket.UserData: 1+ bytes (1+ length prefix, 0+ payload)
* FormsAuthenticationTicket.CookiePath: 1+ bytes (1+ length prefix, 0+ payload)
* {footer}: 1 byte
*/
internal static class FormsAuthenticationTicketSerializer {
private const byte CURRENT_TICKET_SERIALIZED_VERSION = 0x01;
// Resurrects a FormsAuthenticationTicket from its serialized blob representation.
// The input blob must be unsigned and unencrypted. This function returns null if
// the serialized ticket format is invalid. The caller must also verify that the
// ticket is still valid, as this method doesn't check expiration.
public static FormsAuthenticationTicket Deserialize(byte[] serializedTicket, int serializedTicketLength) {
try {
using (MemoryStream ticketBlobStream = new MemoryStream(serializedTicket)) {
using (SerializingBinaryReader ticketReader = new SerializingBinaryReader(ticketBlobStream)) {
// Step 1: Read the serialized format version number from the stream.
// Currently the only supported format is 0x01.
// LENGTH: 1 byte
byte serializedFormatVersion = ticketReader.ReadByte();
if (serializedFormatVersion != CURRENT_TICKET_SERIALIZED_VERSION) {
return null; // unexpected value
}
// Step 2: Read the ticket version number from the stream.
// LENGTH: 1 byte
int ticketVersion = ticketReader.ReadByte();
// Step 3: Read the ticket issue date from the stream.
// LENGTH: 8 bytes
long ticketIssueDateUtcTicks = ticketReader.ReadInt64();
DateTime ticketIssueDateUtc = new DateTime(ticketIssueDateUtcTicks, DateTimeKind.Utc);
DateTime ticketIssueDateLocal = ticketIssueDateUtc.ToLocalTime();
// Step 4: Read the spacer from the stream.
// LENGTH: 1 byte
byte spacer = ticketReader.ReadByte();
if (spacer != 0xfe) {
return null; // unexpected value
}
// Step 5: Read the ticket expiration date from the stream.
// LENGTH: 8 bytes
long ticketExpirationDateUtcTicks = ticketReader.ReadInt64();
DateTime ticketExpirationDateUtc = new DateTime(ticketExpirationDateUtcTicks, DateTimeKind.Utc);
DateTime ticketExpirationDateLocal = ticketExpirationDateUtc.ToLocalTime();
// Step 6: Read the ticket persistence field from the stream.
// LENGTH: 1 byte
byte ticketPersistenceFieldValue = ticketReader.ReadByte();
bool ticketIsPersistent;
switch (ticketPersistenceFieldValue) {
case 0:
ticketIsPersistent = false;
break;
case 1:
ticketIsPersistent = true;
break;
default:
return null; // unexpected value
}
// Step 7: Read the ticket username from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketName = ticketReader.ReadBinaryString();
// Step 8: Read the ticket custom data from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketUserData = ticketReader.ReadBinaryString();
// Step 9: Read the ticket cookie path from the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
string ticketCookiePath = ticketReader.ReadBinaryString();
// Step 10: Read the footer from the stream.
// LENGTH: 1 byte
byte footer = ticketReader.ReadByte();
if (footer != 0xff) {
return null; // unexpected value
}
// Step 11: Verify that we have consumed the entire payload.
// We don't expect there to be any more information after the footer.
// The caller is responsible for telling us when the actual payload
// is finished, as he may have handed us a byte array that contains
// the payload plus signature as an optimization, and we don't want
// to misinterpet the signature as a continuation of the payload.
if (ticketBlobStream.Position != serializedTicketLength) {
return null;
}
// Success.
return FormsAuthenticationTicket.FromUtc(
ticketVersion /* version */,
ticketName /* name */,
ticketIssueDateUtc /* issueDateUtc */,
ticketExpirationDateUtc /* expirationUtc */,
ticketIsPersistent /* isPersistent */,
ticketUserData /* userData */,
ticketCookiePath /* cookiePath */);
}
}
}
catch {
// If anything goes wrong while parsing the token, just treat the token as invalid.
return null;
}
}
// Turns a FormsAuthenticationTicket into a serialized blob.
// The resulting blob is not encrypted or signed.
public static byte[] Serialize(FormsAuthenticationTicket ticket) {
using (MemoryStream ticketBlobStream = new MemoryStream()) {
using (SerializingBinaryWriter ticketWriter = new SerializingBinaryWriter(ticketBlobStream)) {
// SECURITY NOTE:
// Earlier versions of the serializer (Framework20 / Framework40) wrote out a
// random 8-byte header as the first part of the payload. This random header
// was used as an IV when the ticket was encrypted, since the early encryption
// routines didn't automatically append an IV when encrypting data. However,
// the MSRC 10405 (Pythia) patch causes all of our crypto routines to use an
// IV automatically, so there's no need for us to include a random IV in the
// serialized stream any longer. We can just write out only the data, and the
// crypto routines will do the right thing.
// Step 1: Write the ticket serialized format version number (currently 0x01) to the stream.
// LENGTH: 1 byte
ticketWriter.Write(CURRENT_TICKET_SERIALIZED_VERSION);
// Step 2: Write the ticket version number to the stream.
// This is the developer-specified FormsAuthenticationTicket.Version property,
// which is just ticket metadata. Technically it should be stored as a 32-bit
// integer instead of just a byte, but we have historically been storing it
// as just a single byte forever and nobody has complained.
// LENGTH: 1 byte
ticketWriter.Write((byte)ticket.Version);
// Step 3: Write the ticket issue date to the stream.
// We store this value as UTC ticks. We can't use DateTime.ToBinary() since it
// isn't compatible with .NET v1.1.
// LENGTH: 8 bytes (64-bit little-endian in payload)
ticketWriter.Write(ticket.IssueDateUtc.Ticks);
// Step 4: Write a one-byte spacer (0xfe) to the stream.
// One of the old ticket formats (Framework40) expects the unencrypted payload
// to contain 0x000000 (3 null bytes) beginning at position 9 in the stream.
// Since we're currently at offset 10 in the serialized stream, we can take
// this opportunity to purposely inject a non-null byte at this offset, which
// intentionally breaks compatibility with Framework40 mode.
// LENGTH: 1 byte
Debug.Assert(ticketBlobStream.Position == 10, "Critical that we be at position 10 in the stream at this point.");
ticketWriter.Write((byte)0xfe);
// Step 5: Write the ticket expiration date to the stream.
// We store this value as UTC ticks.
// LENGTH: 8 bytes (64-bit little endian in payload)
ticketWriter.Write(ticket.ExpirationUtc.Ticks);
// Step 6: Write the ticket persistence field to the stream.
// LENGTH: 1 byte
ticketWriter.Write(ticket.IsPersistent);
// Step 7: Write the ticket username to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.Name);
// Step 8: Write the ticket custom data to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.UserData);
// Step 9: Write the ticket cookie path to the stream.
// LENGTH: 1+ bytes (7-bit encoded integer char count + UTF-16LE payload)
ticketWriter.WriteBinaryString(ticket.CookiePath);
// Step 10: Write a one-byte footer (0xff) to the stream.
// One of the old FormsAuthenticationTicket formats (Framework20) requires
// that the payload end in 0x0000 (U+0000). By making the very last byte
// of this format non-null, we can guarantee a compatiblity break between
// this format and Framework20.
// LENGTH: 1 byte
ticketWriter.Write((byte)0xff);
// Finished.
return ticketBlobStream.ToArray();
}
}
}
// see comments on SerializingBinaryWriter
private sealed class SerializingBinaryReader : BinaryReader {
public SerializingBinaryReader(Stream input)
: base(input) {
}
public string ReadBinaryString() {
int charCount = Read7BitEncodedInt();
byte[] bytes = ReadBytes(charCount * 2);
char[] chars = new char[charCount];
for (int i = 0; i < chars.Length; i++) {
chars[i] = (char)(bytes[2 * i] | (bytes[2 * i + 1] << 8));
}
return new String(chars);
}
public override string ReadString() {
// should never call this method since it will produce wrong results
throw new NotImplementedException();
}
}
// This is a special BinaryWriter which serializes strings in a way that is
// entirely round-trippable. For example, the string "\ud800" is a valid .NET
// Framework string, but since U+D800 is an unpaired Unicode surrogate the
// built-in Encoding types will not round-trip it. Strings are serialized as a
// 7-bit character count (not byte count!) followed by a UTF-16LE payload.
private sealed class SerializingBinaryWriter : BinaryWriter {
public SerializingBinaryWriter(Stream output)
: base(output) {
}
public override void Write(string value) {
// should never call this method since it will produce wrong results
throw new NotImplementedException();
}
public void WriteBinaryString(string value) {
byte[] bytes = new byte[value.Length * 2];
for (int i = 0; i < value.Length; i++) {
char c = value[i];
bytes[2 * i] = (byte)c;
bytes[2 * i + 1] = (byte)(c >> 8);
}
Write7BitEncodedInt(value.Length);
Write(bytes);
}
}
}
}
|