Skip to content

Commit 7564c06

Browse files
authored
Merge pull request #100 from salesforce/no-re-parser
No-RegExp parser. Integration tested with latest `jsdom` and `request`
2 parents 12d4266 + 751da6d commit 7564c06

File tree

3 files changed

+300
-95
lines changed

3 files changed

+300
-95
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ else
134134
cookies = [Cookie.parse(res.headers['set-cookie'])];
135135
```
136136

137-
_Potentially non-standard behavior:_ currently, tough-cookie will limit the number of spaces before the `=` to 256 characters.
137+
_Note:_ in version 2.3.3, tough-cookie limited the number of spaces before the `=` to 256 characters. This limitation has since been removed.
138138
See [Issue 92](https://github.com/salesforce/tough-cookie/issues/92)
139139

140140
### Properties

lib/cookie.js

Lines changed: 174 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -44,37 +44,24 @@ try {
4444
console.warn("cookie: can't load punycode; won't use punycode for domain normalization");
4545
}
4646

47-
var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/;
48-
4947
// From RFC6265 S4.1.1
5048
// note that it excludes \x3B ";"
51-
var COOKIE_OCTET = /[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/;
52-
var COOKIE_OCTETS = new RegExp('^'+COOKIE_OCTET.source+'+$');
49+
var COOKIE_OCTETS = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/;
5350

5451
var CONTROL_CHARS = /[\x00-\x1F]/;
5552

56-
// For COOKIE_PAIR and LOOSE_COOKIE_PAIR below, the number of spaces has been
57-
// restricted to 256 to side-step a ReDoS issue reported here:
58-
// https://github.com/salesforce/tough-cookie/issues/92
59-
60-
// Double quotes are part of the value (see: S4.1.1).
61-
// '\r', '\n' and '\0' should be treated as a terminator in the "relaxed" mode
62-
// (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60)
63-
// '=' and ';' are attribute/values separators
64-
// (see: https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L64)
65-
var COOKIE_PAIR = /^(([^=;]+))\s{0,256}=\s*([^\n\r\0]*)/;
66-
67-
// Used to parse non-RFC-compliant cookies like '=abc' when given the `loose`
68-
// option in Cookie.parse:
69-
var LOOSE_COOKIE_PAIR = /^((?:=)?([^=;]*)\s{0,256}=\s*)?([^\n\r\0]*)/;
53+
// From Chromium // '\r', '\n' and '\0' should be treated as a terminator in
54+
// the "relaxed" mode, see:
55+
// https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60
56+
var TERMINATORS = ['\n', '\r', '\0'];
7057

7158
// RFC6265 S4.1.1 defines path value as 'any CHAR except CTLs or ";"'
7259
// Note ';' is \x3B
7360
var PATH_VALUE = /[\x20-\x3A\x3C-\x7E]+/;
7461

75-
var DAY_OF_MONTH = /^(\d{1,2})[^\d]*$/;
76-
var TIME = /^(\d{1,2})[^\d]*:(\d{1,2})[^\d]*:(\d{1,2})[^\d]*$/;
77-
var MONTH = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i;
62+
// date-time parsing constants (RFC6265 S5.1.1)
63+
64+
var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/;
7865

7966
var MONTH_TO_NUM = {
8067
jan:0, feb:1, mar:2, apr:3, may:4, jun:5,
@@ -87,13 +74,80 @@ var NUM_TO_DAY = [
8774
'Sun','Mon','Tue','Wed','Thu','Fri','Sat'
8875
];
8976

90-
var YEAR = /^(\d{2}|\d{4})$/; // 2 to 4 digits
91-
9277
var MAX_TIME = 2147483647000; // 31-bit max
9378
var MIN_TIME = 0; // 31-bit min
9479

80+
/*
81+
* Parses a Natural number (i.e., non-negative integer) with either the
82+
* <min>*<max>DIGIT ( non-digit *OCTET )
83+
* or
84+
* <min>*<max>DIGIT
85+
* grammar (RFC6265 S5.1.1).
86+
*
87+
* The "trailingOK" boolean controls if the grammar accepts a
88+
* "( non-digit *OCTET )" trailer.
89+
*/
90+
function parseDigits(token, minDigits, maxDigits, trailingOK) {
91+
var count = 0;
92+
while (count < token.length) {
93+
var c = token.charCodeAt(count);
94+
// "non-digit = %x00-2F / %x3A-FF"
95+
if (c <= 0x2F || c >= 0x3A) {
96+
break;
97+
}
98+
count++;
99+
}
100+
101+
// constrain to a minimum and maximum number of digits.
102+
if (count < minDigits || count > maxDigits) {
103+
return null;
104+
}
105+
106+
if (!trailingOK && count != token.length) {
107+
return null;
108+
}
95109

96-
// RFC6265 S5.1.1 date parser:
110+
return parseInt(token.substr(0,count), 10);
111+
}
112+
113+
function parseTime(token) {
114+
var parts = token.split(':');
115+
var result = [0,0,0];
116+
117+
/* RF6256 S5.1.1:
118+
* time = hms-time ( non-digit *OCTET )
119+
* hms-time = time-field ":" time-field ":" time-field
120+
* time-field = 1*2DIGIT
121+
*/
122+
123+
if (parts.length !== 3) {
124+
return null;
125+
}
126+
127+
for (var i = 0; i < 3; i++) {
128+
// "time-field" must be strictly "1*2DIGIT", HOWEVER, "hms-time" can be
129+
// followed by "( non-digit *OCTET )" so therefore the last time-field can
130+
// have a trailer
131+
var trailingOK = (i == 2);
132+
var num = parseDigits(parts[i], 1, 2, trailingOK);
133+
if (num === null) {
134+
return null;
135+
}
136+
result[i] = num;
137+
}
138+
139+
return result;
140+
}
141+
142+
function parseMonth(token) {
143+
token = String(token).substr(0,3).toLowerCase();
144+
var num = MONTH_TO_NUM[token];
145+
return num >= 0 ? num : null;
146+
}
147+
148+
/*
149+
* RFC6265 S5.1.1 date parser (see RFC for full grammar)
150+
*/
97151
function parseDate(str) {
98152
if (!str) {
99153
return;
@@ -109,9 +163,9 @@ function parseDate(str) {
109163
}
110164

111165
var hour = null;
112-
var minutes = null;
113-
var seconds = null;
114-
var day = null;
166+
var minute = null;
167+
var second = null;
168+
var dayOfMonth = null;
115169
var month = null;
116170
var year = null;
117171

@@ -129,22 +183,12 @@ function parseDate(str) {
129183
* the date-token, respectively. Skip the remaining sub-steps and continue
130184
* to the next date-token.
131185
*/
132-
if (seconds === null) {
133-
result = TIME.exec(token);
186+
if (second === null) {
187+
result = parseTime(token);
134188
if (result) {
135-
hour = parseInt(result[1], 10);
136-
minutes = parseInt(result[2], 10);
137-
seconds = parseInt(result[3], 10);
138-
/* RFC6265 S5.1.1.5:
139-
* [fail if]
140-
* * the hour-value is greater than 23,
141-
* * the minute-value is greater than 59, or
142-
* * the second-value is greater than 59.
143-
*/
144-
if(hour > 23 || minutes > 59 || seconds > 59) {
145-
return;
146-
}
147-
189+
hour = result[0];
190+
minute = result[1];
191+
second = result[2];
148192
continue;
149193
}
150194
}
@@ -154,16 +198,11 @@ function parseDate(str) {
154198
* the day-of-month-value to the number denoted by the date-token. Skip
155199
* the remaining sub-steps and continue to the next date-token.
156200
*/
157-
if (day === null) {
158-
result = DAY_OF_MONTH.exec(token);
159-
if (result) {
160-
day = parseInt(result, 10);
161-
/* RFC6265 S5.1.1.5:
162-
* [fail if] the day-of-month-value is less than 1 or greater than 31
163-
*/
164-
if(day < 1 || day > 31) {
165-
return;
166-
}
201+
if (dayOfMonth === null) {
202+
// "day-of-month = 1*2DIGIT ( non-digit *OCTET )"
203+
result = parseDigits(token, 1, 2, true);
204+
if (result !== null) {
205+
dayOfMonth = result;
167206
continue;
168207
}
169208
}
@@ -174,47 +213,63 @@ function parseDate(str) {
174213
* continue to the next date-token.
175214
*/
176215
if (month === null) {
177-
result = MONTH.exec(token);
178-
if (result) {
179-
month = MONTH_TO_NUM[result[1].toLowerCase()];
216+
result = parseMonth(token);
217+
if (result !== null) {
218+
month = result;
180219
continue;
181220
}
182221
}
183222

184-
/* 2.4. If the found-year flag is not set and the date-token matches the year
185-
* production, set the found-year flag and set the year-value to the number
186-
* denoted by the date-token. Skip the remaining sub-steps and continue to
187-
* the next date-token.
223+
/* 2.4. If the found-year flag is not set and the date-token matches the
224+
* year production, set the found-year flag and set the year-value to the
225+
* number denoted by the date-token. Skip the remaining sub-steps and
226+
* continue to the next date-token.
188227
*/
189228
if (year === null) {
190-
result = YEAR.exec(token);
191-
if (result) {
192-
year = parseInt(result[0], 10);
229+
// "year = 2*4DIGIT ( non-digit *OCTET )"
230+
result = parseDigits(token, 2, 4, true);
231+
if (result !== null) {
232+
year = result;
193233
/* From S5.1.1:
194234
* 3. If the year-value is greater than or equal to 70 and less
195235
* than or equal to 99, increment the year-value by 1900.
196236
* 4. If the year-value is greater than or equal to 0 and less
197237
* than or equal to 69, increment the year-value by 2000.
198238
*/
199-
if (70 <= year && year <= 99) {
239+
if (year >= 70 && year <= 99) {
200240
year += 1900;
201-
} else if (0 <= year && year <= 69) {
241+
} else if (year >= 0 && year <= 69) {
202242
year += 2000;
203243
}
204-
205-
if (year < 1601) {
206-
return; // 5. ... the year-value is less than 1601
207-
}
208244
}
209245
}
210246
}
211247

212-
if (seconds === null || day === null || month === null || year === null) {
213-
return; // 5. ... at least one of the found-day-of-month, found-month, found-
214-
// year, or found-time flags is not set,
248+
/* RFC 6265 S5.1.1
249+
* "5. Abort these steps and fail to parse the cookie-date if:
250+
* * at least one of the found-day-of-month, found-month, found-
251+
* year, or found-time flags is not set,
252+
* * the day-of-month-value is less than 1 or greater than 31,
253+
* * the year-value is less than 1601,
254+
* * the hour-value is greater than 23,
255+
* * the minute-value is greater than 59, or
256+
* * the second-value is greater than 59.
257+
* (Note that leap seconds cannot be represented in this syntax.)"
258+
*
259+
* So, in order as above:
260+
*/
261+
if (
262+
dayOfMonth === null || month === null || year === null || second === null ||
263+
dayOfMonth < 1 || dayOfMonth > 31 ||
264+
year < 1601 ||
265+
hour > 23 ||
266+
minute > 59 ||
267+
second > 59
268+
) {
269+
return;
215270
}
216271

217-
return new Date(Date.UTC(year, month, day, hour, minutes, seconds));
272+
return new Date(Date.UTC(year, month, dayOfMonth, hour, minute, second));
218273
}
219274

220275
function formatDate(date) {
@@ -321,32 +376,62 @@ function defaultPath(path) {
321376
return path.slice(0, rightSlash);
322377
}
323378

379+
function trimTerminator(str) {
380+
for (var t = 0; t < TERMINATORS.length; t++) {
381+
var terminatorIdx = str.indexOf(TERMINATORS[t]);
382+
if (terminatorIdx !== -1) {
383+
str = str.substr(0,terminatorIdx);
384+
}
385+
}
324386

325-
function parse(str, options) {
326-
if (!options || typeof options !== 'object') {
327-
options = {};
387+
return str;
388+
}
389+
390+
function parseCookiePair(cookiePair, looseMode) {
391+
cookiePair = trimTerminator(cookiePair);
392+
393+
var firstEq = cookiePair.indexOf('=');
394+
if (looseMode) {
395+
if (firstEq === 0) { // '=' is immediately at start
396+
cookiePair = cookiePair.substr(1);
397+
firstEq = cookiePair.indexOf('='); // might still need to split on '='
398+
}
399+
} else { // non-loose mode
400+
if (firstEq <= 0) { // no '=' or is at start
401+
return; // needs to have non-empty "cookie-name"
402+
}
328403
}
329-
str = str.trim();
330404

331-
// We use a regex to parse the "name-value-pair" part of S5.2
332-
var firstSemi = str.indexOf(';'); // S5.2 step 1
333-
var pairRe = options.loose ? LOOSE_COOKIE_PAIR : COOKIE_PAIR;
334-
var result = pairRe.exec(firstSemi === -1 ? str : str.substr(0,firstSemi));
405+
var cookieName, cookieValue;
406+
if (firstEq <= 0) {
407+
cookieName = "";
408+
cookieValue = cookiePair.trim();
409+
} else {
410+
cookieName = cookiePair.substr(0, firstEq).trim();
411+
cookieValue = cookiePair.substr(firstEq+1).trim();
412+
}
335413

336-
// Rx satisfies the "the name string is empty" and "lacks a %x3D ("=")"
337-
// constraints as well as trimming any whitespace.
338-
if (!result) {
414+
if (CONTROL_CHARS.test(cookieName) || CONTROL_CHARS.test(cookieValue)) {
339415
return;
340416
}
341417

342418
var c = new Cookie();
343-
if (result[1]) {
344-
c.key = result[2].trim();
345-
} else {
346-
c.key = '';
419+
c.key = cookieName;
420+
c.value = cookieValue;
421+
return c;
422+
}
423+
424+
function parse(str, options) {
425+
if (!options || typeof options !== 'object') {
426+
options = {};
347427
}
348-
c.value = result[3].trim();
349-
if (CONTROL_CHARS.test(c.key) || CONTROL_CHARS.test(c.value)) {
428+
str = str.trim();
429+
430+
// We use a regex to parse the "name-value-pair" part of S5.2
431+
var firstSemi = str.indexOf(';'); // S5.2 step 1
432+
var cookiePair = (firstSemi === -1) ? str : str.substr(0, firstSemi);
433+
var c = parseCookiePair(cookiePair, !!options.loose);
434+
if (!c) {
350435
return;
351436
}
352437

0 commit comments

Comments
 (0)