e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
280 lines
15 KiB
C#
280 lines
15 KiB
C#
//------------------------------------------------------------------------------
|
|
// <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);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|