File: FormsAuthenticationTicketSerializer.cs

package info (click to toggle)
mono 6.8.0.105%2Bdfsg-3.3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,284,512 kB
  • sloc: cs: 11,172,132; xml: 2,850,069; ansic: 671,653; cpp: 122,091; perl: 59,366; javascript: 30,841; asm: 22,168; makefile: 20,093; sh: 15,020; python: 4,827; pascal: 925; sql: 859; sed: 16; php: 1
file content (279 lines) | stat: -rw-r--r-- 14,957 bytes parent folder | download | duplicates (7)
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);
            }
        }

    }
}