[BlackBerry] Add a check to filter out cookies that tries to set the domain to a...
[WebKit-https.git] / Source / WebCore / platform / blackberry / CookieParser.cpp
1 /*
2  * Copyright (C) 2009 Julien Chaffraix <jchaffraix@pleyo.com>
3  * Copyright (C) 2010, 2011, 2012 Research In Motion Limited. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
15  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
18  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25  */ 
26
27 #include "config.h"
28 #include "CookieParser.h"
29
30 #include "Logging.h"
31 #include "ParsedCookie.h"
32 #include <network/DomainTools.h>
33 #include <wtf/CurrentTime.h>
34 #include <wtf/text/CString.h>
35
36 namespace WebCore {
37
38 #define LOG_AND_DELETE(format, ...) \
39     { \
40         LOG_ERROR(format, ## __VA_ARGS__); \
41         delete res; \
42         return 0; \
43     }
44
45 static inline bool isCookieHeaderSeparator(UChar c)
46 {
47     return (c == '\r' || c =='\n');
48 }
49
50 static inline bool isLightweightSpace(UChar c)
51 {
52     return (c == ' ' || c == '\t');
53 }
54
55 CookieParser::CookieParser(const KURL& defaultCookieURL)
56     : m_defaultCookieURL(defaultCookieURL)
57 {
58 }
59
60 CookieParser::~CookieParser()
61 {
62 }
63
64 Vector<ParsedCookie*> CookieParser::parse(const String& cookies)
65 {
66     unsigned cookieStart, cookieEnd = 0;
67     double curTime = currentTime();
68     Vector<ParsedCookie*, 4> parsedCookies;
69
70     unsigned cookiesLength = cookies.length();
71     if (!cookiesLength) // Code below doesn't handle this case
72         return parsedCookies;
73
74     // Iterate over the header to parse all the cookies.
75     while (cookieEnd <= cookiesLength) {
76         cookieStart = cookieEnd;
77         
78         // Find a cookie separator.
79         while (cookieEnd <= cookiesLength && !isCookieHeaderSeparator(cookies[cookieEnd]))
80             cookieEnd++;
81
82         // Detect an empty cookie and go to the next one.
83         if (cookieStart == cookieEnd) {
84             ++cookieEnd;
85             continue;
86         }
87
88         if (cookieEnd < cookiesLength && isCookieHeaderSeparator(cookies[cookieEnd]))
89             ++cookieEnd;
90
91         ParsedCookie* cookie = parseOneCookie(cookies, cookieStart, cookieEnd - 1, curTime);
92         if (cookie)
93             parsedCookies.append(cookie);
94     }
95     return parsedCookies;
96 }
97
98 // The cookie String passed into this method will only contian the name value pairs as well as other related cookie
99 // attributes such as max-age and domain. Set-Cookie should never be part of this string.
100 ParsedCookie* CookieParser::parseOneCookie(const String& cookie, unsigned start, unsigned end, double curTime)
101 {
102     ParsedCookie* res = new ParsedCookie(curTime);
103
104     if (!res)
105         LOG_AND_DELETE("Out of memory");
106
107     res->setProtocol(m_defaultCookieURL.protocol());
108
109     // Parse [NAME "="] VALUE
110     unsigned tokenEnd = start; // Token end contains the position of the '=' or the end of a token
111     unsigned pairEnd = start; // Pair end contains always the position of the ';'
112
113     // Find the first ';' which is not double-quoted and the '=' (if they exist).
114     bool foundEqual = false;
115     while (pairEnd < end && cookie[pairEnd] != ';') {
116         if (cookie[pairEnd] == '=') {
117             if (tokenEnd == start) {
118                 tokenEnd = pairEnd;
119                 foundEqual = true;
120             }
121         } else if (cookie[pairEnd] == '"') {
122             size_t secondQuotePosition = cookie.find('"', pairEnd + 1);
123             if (secondQuotePosition != notFound && secondQuotePosition <= end) {
124                 pairEnd = secondQuotePosition + 1;
125                 continue;
126             }
127         }
128         pairEnd++;
129     }
130
131     unsigned tokenStart = start;
132
133     bool hasName = false; // This is a hack to avoid changing too much in this
134                           // brutally brittle code.
135     if (tokenEnd != start) {
136         // There is a '=' so parse the NAME
137         unsigned nameEnd = tokenEnd;
138
139         // The tokenEnd is the position of the '=' so the nameEnd is one less
140         nameEnd--;
141
142         // Remove lightweight spaces.
143         while (nameEnd && isLightweightSpace(cookie[nameEnd]))
144             nameEnd--;
145
146         while (tokenStart < nameEnd && isLightweightSpace(cookie[tokenStart]))
147             tokenStart++;
148
149         if (nameEnd + 1 <= tokenStart)
150             LOG_AND_DELETE("Empty name. Rejecting the cookie");
151
152         String name = cookie.substring(tokenStart, nameEnd + 1 - start);
153         res->setName(name);
154         hasName = true;
155     }
156
157     // Now parse the VALUE
158     tokenStart = tokenEnd + 1;
159     if (!hasName)
160         --tokenStart;
161
162     // Skip lightweight spaces in our token
163     while (tokenStart < pairEnd && isLightweightSpace(cookie[tokenStart]))
164         tokenStart++;
165
166     tokenEnd = pairEnd;
167     while (tokenEnd > tokenStart && isLightweightSpace(cookie[tokenEnd - 1]))
168         tokenEnd--;
169
170     String value;
171     if (tokenEnd == tokenStart) {
172         // Firefox accepts empty value so we will do the same
173         value = String();
174     } else
175         value = cookie.substring(tokenStart, tokenEnd - tokenStart);
176
177     if (hasName)
178         res->setValue(value);
179     else if (foundEqual) {
180         delete res;
181         return 0;
182     } else
183         res->setName(value); // No NAME=VALUE, only NAME
184
185     while (pairEnd < end) {
186         // Switch to the next pair as pairEnd is on the ';' and fast-forward any lightweight spaces.
187         pairEnd++;
188         while (pairEnd < end && isLightweightSpace(cookie[pairEnd]))
189             pairEnd++;
190
191         tokenStart = pairEnd;
192         tokenEnd = tokenStart; // initialize token end to catch first '='
193
194         while (pairEnd < end && cookie[pairEnd] != ';') {
195             if (tokenEnd == tokenStart && cookie[pairEnd] == '=')
196                 tokenEnd = pairEnd;
197             pairEnd++;
198         }
199
200         // FIXME : should we skip lightweight spaces here ?
201
202         unsigned length = tokenEnd - tokenStart;
203         unsigned tokenStartSvg = tokenStart;
204
205         String parsedValue;
206         if (tokenStart != tokenEnd) {
207             // There is an equal sign so remove lightweight spaces in VALUE
208             tokenStart = tokenEnd + 1;
209             while (tokenStart < pairEnd && isLightweightSpace(cookie[tokenStart]))
210                 tokenStart++;
211
212             tokenEnd = pairEnd;
213             while (tokenEnd > tokenStart && isLightweightSpace(cookie[tokenEnd - 1]))
214                 tokenEnd--;
215
216             parsedValue = cookie.substring(tokenStart, tokenEnd - tokenStart);
217         } else {
218             // If the parsedValue is empty, initialise it in case we need it
219             parsedValue = String();
220             // Handle a token without value.
221             length = pairEnd - tokenStart;
222         }
223
224        // Detect which "cookie-av" is parsed
225        // Look at the first char then parse the whole for performance issue
226         switch (cookie[tokenStartSvg]) {
227         case 'P':
228         case 'p' : {
229             if (length >= 4 && cookie.find("ath", tokenStartSvg + 1, false)) {
230                 // We need the path to be decoded to match those returned from KURL::path().
231                 // The path attribute may or may not include percent-encoded characters. Fortunately
232                 // if there are no percent-encoded characters, decoding the url is a no-op.
233                 res->setPath(decodeURLEscapeSequences(parsedValue));
234
235                 // We have to disable the following check because sites like Facebook and
236                 // Gmail currently do not follow the spec.
237 #if 0
238                 // Check if path attribute is a prefix of the request URI.
239                 if (!m_defaultCookieURL.path().startsWith(res->path()))
240                     LOG_AND_DELETE("Invalid cookie %s (path): it does not math the URL", cookie.ascii().data());
241 #endif
242
243             } else
244                 LOG_AND_DELETE("Invalid cookie %s (path)", cookie.ascii().data());
245             break;
246         }
247
248         case 'D':
249         case 'd' : {
250             if (length >= 6 && cookie.find("omain", tokenStartSvg + 1, false)) {
251                 if (parsedValue.length() > 1 && parsedValue[0] == '"' && parsedValue[parsedValue.length() - 1] == '"')
252                     parsedValue = parsedValue.substring(1, parsedValue.length() - 2);
253
254                 // Check if the domain contains an embedded dot.
255                 size_t dotPosition = parsedValue.find(".", 1);
256                 if (dotPosition == notFound || dotPosition == parsedValue.length())
257                     LOG_AND_DELETE("Invalid cookie %s (domain): it does not contain an embedded dot", cookie.ascii().data());
258
259                 // If the domain does not start with a dot, add one for security checks,
260                 // For example: ab.c.com dose not domain match b.c.com;
261                 String realDomain = parsedValue[0] == '.' ? parsedValue : "." + parsedValue;
262
263                 // The request host should domain match the Domain attribute.
264                 // Domain string starts with a dot, so a.b.com should domain match .a.b.com.
265                 // add a "." at beginning of host name, because it can handle many cases such as
266                 // a.b.com matches b.com, a.b.com matches .B.com and a.b.com matches .A.b.Com
267                 // and so on.
268                 String hostDomainName = m_defaultCookieURL.host();
269                 hostDomainName = hostDomainName.startsWith('.') ? hostDomainName : "." + hostDomainName;
270                 if (!hostDomainName.endsWith(realDomain, false))
271                     LOG_AND_DELETE("Invalid cookie %s (domain): it does not domain match the host");
272                 // We should check for an embedded dot in the portion of string in the host not in the domain
273                 // but to match firefox behaviour we do not.
274
275                 // Check whether the domain is a top level domain, if it is throw it out
276                 // http://publicsuffix.org/list/
277                 if (BlackBerry::Platform::isTopLevelDomain(realDomain.utf8().data()))
278                     LOG_AND_DELETE("Invalid cookie %s (domain): it did not pass the top level domain check", cookie.ascii().data());
279
280                 res->setDomain(realDomain);
281             } else
282                 LOG_AND_DELETE("Invalid cookie %s (domain)", cookie.ascii().data());
283             break;
284         }
285
286         case 'E' :
287         case 'e' : {
288             if (length >= 7 && cookie.find("xpires", tokenStartSvg + 1, false))
289                 res->setExpiry(parsedValue);
290             else
291                 LOG_AND_DELETE("Invalid cookie %s (expires)", cookie.ascii().data());
292             break;
293         }
294
295         case 'M' :
296         case 'm' : {
297             if (length >= 7 && cookie.find("ax-age", tokenStartSvg + 1, false))
298                 res->setMaxAge(parsedValue);
299             else
300                 LOG_AND_DELETE("Invalid cookie %s (max-age)", cookie.ascii().data());
301             break;
302         }
303
304         case 'C' :
305         case 'c' : {
306             if (length >= 7 && cookie.find("omment", tokenStartSvg + 1, false))
307                 // We do not have room for the comment part (and so do Mozilla) so just log the comment.
308                 LOG(Network, "Comment %s for ParsedCookie : %s\n", parsedValue.ascii().data(), cookie.ascii().data());
309             else
310                 LOG_AND_DELETE("Invalid cookie %s (comment)", cookie.ascii().data());
311             break;
312         }
313
314         case 'V' :
315         case 'v' : {
316             if (length >= 7 && cookie.find("ersion", tokenStartSvg + 1, false)) {
317                 // Although the out-of-dated Cookie Spec(RFC2965, http://tools.ietf.org/html/rfc2965) defined
318                 // the value of version can only contain DIGIT, some random sites, e.g. https://devforums.apple.com
319                 // would use double quotation marks to quote the digit. So we need to get rid of them for compliance.
320                 if (parsedValue.length() > 1 && parsedValue[0] == '"' && parsedValue[parsedValue.length() - 1] == '"')
321                     parsedValue = parsedValue.substring(1, parsedValue.length() - 2);
322
323                 if (parsedValue.toInt() != 1)
324                     LOG_AND_DELETE("ParsedCookie version %d not supported (only support version=1)", parsedValue.toInt());
325             } else
326                 LOG_AND_DELETE("Invalid cookie %s (version)", cookie.ascii().data());
327             break;
328         }
329
330         case 'S' :
331         case 's' : {
332             // Secure is a standalone token ("Secure;")
333             if (length >= 6 && cookie.find("ecure", tokenStartSvg + 1, false))
334                 res->setSecureFlag(true);
335             else
336                 LOG_AND_DELETE("Invalid cookie %s (secure)", cookie.ascii().data());
337             break;
338         }
339         case 'H':
340         case 'h': {
341             // HttpOnly is a standalone token ("HttpOnly;")
342             if (length >= 8 && cookie.find("ttpOnly", tokenStartSvg + 1, false))
343                 res->setIsHttpOnly(true);
344             else
345                 LOG_AND_DELETE("Invalid cookie %s (HttpOnly)", cookie.ascii().data());
346             break;
347         }
348
349         default : {
350             // If length == 0, we should be at the end of the cookie (case : ";\r") so ignore it
351             if (length)
352                 LOG_ERROR("Invalid token for cookie %s", cookie.ascii().data());
353         }
354         }
355     }
356
357     // Check if the cookie is valid with respect to the size limit.
358     if (!res->isUnderSizeLimit())
359         LOG_AND_DELETE("ParsedCookie %s is above the 4kb in length : REJECTED", cookie.ascii().data());
360
361     // If some pair was not provided, during parsing then apply some default value
362     // the rest has been done in the constructor.
363
364     // If no domain was provided, set it to the host
365     if (!res->domain())
366         res->setDomain(m_defaultCookieURL.host());
367
368     // According to the Cookie Specificaiton (RFC6265, section 4.1.2.4 and 5.2.4, http://tools.ietf.org/html/rfc6265),
369     // If no path was provided or the first character of the path value is not '/', set it to the host's path
370     //
371     // REFERENCE
372     // 4.1.2.4. The Path Attribute
373     //
374     // The scope of each cookie is limited to a set of paths, controlled by
375     // the Path attribute. If the server omits the Path attribute, the user
376     // agent will use the "directory" of the request-uri's path component as
377     // the default value. (See Section 5.1.4 for more details.)
378     // ...........
379     // 5.2.4. The Path Attribute
380     //
381     // If the attribute-name case-insensitively matches the string "Path",
382     // the user agent MUST process the cookie-av as follows.
383     //
384     // If the attribute-value is empty or if the first character of the
385     // attribute-value is not %x2F ("/"):
386     //
387     // Let cookie-path be the default-path.
388     //
389     // Otherwise:
390     //
391     // Let cookie-path be the attribute-value.
392     //
393     // Append an attribute to the cookie-attribute-list with an attribute-
394     // name of Path and an attribute-value of cookie-path.
395     if (!res->path() || !res->path().length() || !res->path().startsWith("/", false)) {
396         String path = m_defaultCookieURL.string().substring(m_defaultCookieURL.pathStart(), m_defaultCookieURL.pathAfterLastSlash() - m_defaultCookieURL.pathStart() - 1);
397         if (path.isEmpty())
398             path = "/";
399         // Since this is reading the raw url string, it could contain percent-encoded sequences. We
400         // want it to be comparable to the return value of url.path(), which is not percent-encoded,
401         // so we must remove the escape sequences.
402         res->setPath(decodeURLEscapeSequences(path));
403     }
404  
405     return res;
406 }
407
408 } // namespace WebCore