test_duplicate_headers.js (17428B)
1 /* 2 * Tests bugs 597706, 655389: prevent duplicate headers with differing values 3 * for some headers like Content-Length, Location, etc. 4 */ 5 6 //////////////////////////////////////////////////////////////////////////////// 7 // Test infrastructure 8 9 "use strict"; 10 11 // The tests in this file use number indexes to run, which can't be detected 12 // via ESLint. 13 /* eslint-disable no-unused-vars */ 14 15 const { HttpServer } = ChromeUtils.importESModule( 16 "resource://testing-common/httpd.sys.mjs" 17 ); 18 19 ChromeUtils.defineLazyGetter(this, "URL", function () { 20 return "http://localhost:" + httpserver.identity.primaryPort; 21 }); 22 23 var httpserver = new HttpServer(); 24 var test_flags = []; 25 var testPathBase = "/dupe_hdrs"; 26 27 function run_test() { 28 httpserver.start(-1); 29 30 do_test_pending(); 31 run_test_number(1); 32 } 33 34 function run_test_number(num) { 35 let testPath = testPathBase + num; 36 httpserver.registerPathHandler(testPath, globalThis["handler" + num]); 37 38 var channel = setupChannel(testPath); 39 let flags = test_flags[num]; // OK if flags undefined for test 40 channel.asyncOpen( 41 new ChannelListener(globalThis["completeTest" + num], channel, flags) 42 ); 43 } 44 45 function setupChannel(url) { 46 var chan = NetUtil.newChannel({ 47 uri: URL + url, 48 loadUsingSystemPrincipal: true, 49 }); 50 var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); 51 return httpChan; 52 } 53 54 function endTests() { 55 httpserver.stop(do_test_finished); 56 } 57 58 //////////////////////////////////////////////////////////////////////////////// 59 // Test 1: FAIL because of conflicting Content-Length headers 60 test_flags[1] = CL_EXPECT_FAILURE; 61 62 function handler1(metadata, response) { 63 var body = "012345678901234567890123456789"; 64 // Comrades! We must seize power from the petty-bourgeois running dogs of 65 // httpd.js in order to reply with multiple instances of the same header! 66 response.seizePower(); 67 response.write("HTTP/1.0 200 OK\r\n"); 68 response.write("Content-Type: text/plain\r\n"); 69 response.write("Content-Length: 30\r\n"); 70 response.write("Content-Length: 20\r\n"); 71 response.write("\r\n"); 72 response.write(body); 73 response.finish(); 74 } 75 76 function completeTest1(request) { 77 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 78 79 run_test_number(2); 80 } 81 82 //////////////////////////////////////////////////////////////////////////////// 83 // Test 2: OK to have duplicate same Content-Length headers 84 85 function handler2(metadata, response) { 86 var body = "012345678901234567890123456789"; 87 response.seizePower(); 88 response.write("HTTP/1.0 200 OK\r\n"); 89 response.write("Content-Type: text/plain\r\n"); 90 response.write("Content-Length: 30\r\n"); 91 response.write("Content-Length: 30\r\n"); 92 response.write("\r\n"); 93 response.write(body); 94 response.finish(); 95 } 96 97 function completeTest2(request) { 98 Assert.equal(request.status, 0); 99 run_test_number(3); 100 } 101 102 //////////////////////////////////////////////////////////////////////////////// 103 // Test 3: FAIL: 2nd Content-length is blank 104 test_flags[3] = CL_EXPECT_FAILURE; 105 106 function handler3(metadata, response) { 107 var body = "012345678901234567890123456789"; 108 response.seizePower(); 109 response.write("HTTP/1.0 200 OK\r\n"); 110 response.write("Content-Type: text/plain\r\n"); 111 response.write("Content-Length: 30\r\n"); 112 response.write("Content-Length:\r\n"); 113 response.write("\r\n"); 114 response.write(body); 115 response.finish(); 116 } 117 118 function completeTest3(request) { 119 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 120 121 run_test_number(4); 122 } 123 124 //////////////////////////////////////////////////////////////////////////////// 125 // Test 4: ensure that blank C-len header doesn't allow attacker to reset Clen, 126 // then insert CRLF attack 127 test_flags[4] = CL_EXPECT_FAILURE; 128 129 function handler4(metadata, response) { 130 var body = "012345678901234567890123456789"; 131 132 response.seizePower(); 133 response.write("HTTP/1.0 200 OK\r\n"); 134 response.write("Content-Type: text/plain\r\n"); 135 response.write("Content-Length: 30\r\n"); 136 137 // Bad Mr Hacker! Bad! 138 var evilBody = "We are the Evil bytes, Evil bytes, Evil bytes!"; 139 response.write("Content-Length:\r\n"); 140 response.write("Content-Length: %s\r\n\r\n%s" % (evilBody.length, evilBody)); 141 response.write("\r\n"); 142 response.write(body); 143 response.finish(); 144 } 145 146 function completeTest4(request) { 147 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 148 149 run_test_number(5); 150 } 151 152 //////////////////////////////////////////////////////////////////////////////// 153 // Test 5: ensure that we take 1st instance of duplicate, nonmerged headers that 154 // are permitted : (ex: Referrer) 155 156 function handler5(metadata, response) { 157 var body = "012345678901234567890123456789"; 158 response.seizePower(); 159 response.write("HTTP/1.0 200 OK\r\n"); 160 response.write("Content-Type: text/plain\r\n"); 161 response.write("Content-Length: 30\r\n"); 162 response.write("Referer: naive.org\r\n"); 163 response.write("Referer: evil.net\r\n"); 164 response.write("\r\n"); 165 response.write(body); 166 response.finish(); 167 } 168 169 function completeTest5(request) { 170 try { 171 let referer = request.getResponseHeader("Referer"); 172 Assert.equal(referer, "naive.org"); 173 } catch (ex) { 174 do_throw("Referer header should be present"); 175 } 176 177 run_test_number(6); 178 } 179 180 //////////////////////////////////////////////////////////////////////////////// 181 // Test 5: FAIL if multiple, different Location: headers present 182 // - needed to prevent CRLF injection attacks 183 test_flags[6] = CL_EXPECT_FAILURE; 184 185 function handler6(metadata, response) { 186 var body = "012345678901234567890123456789"; 187 response.seizePower(); 188 response.write("HTTP/1.0 301 Moved\r\n"); 189 response.write("Content-Type: text/plain\r\n"); 190 response.write("Content-Length: 30\r\n"); 191 response.write("Location: " + URL + "/content\r\n"); 192 response.write("Location: http://www.microsoft.com/\r\n"); 193 response.write("Connection: close\r\n"); 194 response.write("\r\n"); 195 response.write(body); 196 response.finish(); 197 } 198 199 function completeTest6(request) { 200 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 201 202 // run_test_number(7); // Test 7 leaking under e10s: unrelated bug? 203 run_test_number(8); 204 } 205 206 //////////////////////////////////////////////////////////////////////////////// 207 // Test 7: OK to have multiple Location: headers with same value 208 209 function handler7(metadata, response) { 210 var body = "012345678901234567890123456789"; 211 response.seizePower(); 212 response.write("HTTP/1.0 301 Moved\r\n"); 213 response.write("Content-Type: text/plain\r\n"); 214 response.write("Content-Length: 30\r\n"); 215 // redirect to previous test handler that completes OK: test 5 216 response.write("Location: " + URL + testPathBase + "5\r\n"); 217 response.write("Location: " + URL + testPathBase + "5\r\n"); 218 response.write("Connection: close\r\n"); 219 response.write("\r\n"); 220 response.write(body); 221 response.finish(); 222 } 223 224 function completeTest7(request) { 225 // for some reason need this here 226 request.QueryInterface(Ci.nsIHttpChannel); 227 228 try { 229 let referer = request.getResponseHeader("Referer"); 230 Assert.equal(referer, "naive.org"); 231 } catch (ex) { 232 do_throw("Referer header should be present"); 233 } 234 235 run_test_number(8); 236 } 237 238 //////////////////////////////////////////////////////////////////////////////// 239 // FAIL if 2nd Location: headers blank 240 test_flags[8] = CL_EXPECT_FAILURE; 241 242 function handler8(metadata, response) { 243 var body = "012345678901234567890123456789"; 244 response.seizePower(); 245 response.write("HTTP/1.0 301 Moved\r\n"); 246 response.write("Content-Type: text/plain\r\n"); 247 response.write("Content-Length: 30\r\n"); 248 // redirect to previous test handler that completes OK: test 4 249 response.write("Location: " + URL + testPathBase + "4\r\n"); 250 response.write("Location:\r\n"); 251 response.write("Connection: close\r\n"); 252 response.write("\r\n"); 253 response.write(body); 254 response.finish(); 255 } 256 257 function completeTest8(request) { 258 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 259 260 run_test_number(9); 261 } 262 263 //////////////////////////////////////////////////////////////////////////////// 264 // Test 9: ensure that blank Location header doesn't allow attacker to reset, 265 // then insert an evil one 266 test_flags[9] = CL_EXPECT_FAILURE; 267 268 function handler9(metadata, response) { 269 var body = "012345678901234567890123456789"; 270 response.seizePower(); 271 response.write("HTTP/1.0 301 Moved\r\n"); 272 response.write("Content-Type: text/plain\r\n"); 273 response.write("Content-Length: 30\r\n"); 274 // redirect to previous test handler that completes OK: test 2 275 response.write("Location: " + URL + testPathBase + "2\r\n"); 276 response.write("Location:\r\n"); 277 // redirect to previous test handler that completes OK: test 4 278 response.write("Location: " + URL + testPathBase + "4\r\n"); 279 response.write("Connection: close\r\n"); 280 response.write("\r\n"); 281 response.write(body); 282 response.finish(); 283 } 284 285 function completeTest9(request) { 286 // All redirection should fail: 287 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 288 289 run_test_number(10); 290 } 291 292 //////////////////////////////////////////////////////////////////////////////// 293 // Test 10: FAIL: if conflicting values for Content-Dispo 294 test_flags[10] = CL_EXPECT_FAILURE; 295 296 function handler10(metadata, response) { 297 var body = "012345678901234567890123456789"; 298 response.seizePower(); 299 response.write("HTTP/1.0 200 OK\r\n"); 300 response.write("Content-Type: text/plain\r\n"); 301 response.write("Content-Length: 30\r\n"); 302 response.write("Content-Disposition: attachment; filename=foo\r\n"); 303 response.write("Content-Disposition: attachment; filename=bar\r\n"); 304 response.write("Content-Disposition: attachment; filename=baz\r\n"); 305 response.write("\r\n"); 306 response.write(body); 307 response.finish(); 308 } 309 310 function completeTest10(request) { 311 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 312 313 run_test_number(11); 314 } 315 316 //////////////////////////////////////////////////////////////////////////////// 317 // Test 11: OK to have duplicate same Content-Disposition headers 318 319 function handler11(metadata, response) { 320 var body = "012345678901234567890123456789"; 321 response.seizePower(); 322 response.write("HTTP/1.0 200 OK\r\n"); 323 response.write("Content-Type: text/plain\r\n"); 324 response.write("Content-Length: 30\r\n"); 325 response.write("Content-Disposition: attachment; filename=foo\r\n"); 326 response.write("Content-Disposition: attachment; filename=foo\r\n"); 327 response.write("\r\n"); 328 response.write(body); 329 response.finish(); 330 } 331 332 function completeTest11(request) { 333 Assert.equal(request.status, 0); 334 335 try { 336 var chan = request.QueryInterface(Ci.nsIChannel); 337 Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); 338 Assert.equal(chan.contentDispositionFilename, "foo"); 339 Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo"); 340 } catch (ex) { 341 do_throw("error parsing Content-Disposition: " + ex); 342 } 343 344 run_test_number(12); 345 } 346 347 //////////////////////////////////////////////////////////////////////////////// 348 // Bug 716801 OK for Location: header to be blank 349 350 function handler12(metadata, response) { 351 var body = "012345678901234567890123456789"; 352 response.seizePower(); 353 response.write("HTTP/1.0 200 OK\r\n"); 354 response.write("Content-Type: text/plain\r\n"); 355 response.write("Content-Length: 30\r\n"); 356 response.write("Location:\r\n"); 357 response.write("Connection: close\r\n"); 358 response.write("\r\n"); 359 response.write(body); 360 response.finish(); 361 } 362 363 function completeTest12(request, data) { 364 Assert.equal(request.status, Cr.NS_OK); 365 Assert.equal(30, data.length); 366 367 run_test_number(13); 368 } 369 370 //////////////////////////////////////////////////////////////////////////////// 371 // Negative content length is ok 372 test_flags[13] = CL_ALLOW_UNKNOWN_CL; 373 374 function handler13(metadata, response) { 375 var body = "012345678901234567890123456789"; 376 response.seizePower(); 377 response.write("HTTP/1.0 200 OK\r\n"); 378 response.write("Content-Type: text/plain\r\n"); 379 response.write("Content-Length: -1\r\n"); 380 response.write("Connection: close\r\n"); 381 response.write("\r\n"); 382 response.write(body); 383 response.finish(); 384 } 385 386 function completeTest13(request, data) { 387 Assert.equal(request.status, Cr.NS_OK); 388 Assert.equal(30, data.length); 389 390 run_test_number(14); 391 } 392 393 //////////////////////////////////////////////////////////////////////////////// 394 // leading negative content length is not ok if paired with positive one 395 396 test_flags[14] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; 397 398 function handler14(metadata, response) { 399 var body = "012345678901234567890123456789"; 400 response.seizePower(); 401 response.write("HTTP/1.0 200 OK\r\n"); 402 response.write("Content-Type: text/plain\r\n"); 403 response.write("Content-Length: -1\r\n"); 404 response.write("Content-Length: 30\r\n"); 405 response.write("Connection: close\r\n"); 406 response.write("\r\n"); 407 response.write(body); 408 response.finish(); 409 } 410 411 function completeTest14(request) { 412 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 413 414 run_test_number(15); 415 } 416 417 //////////////////////////////////////////////////////////////////////////////// 418 // trailing negative content length is not ok if paired with positive one 419 420 test_flags[15] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; 421 422 function handler15(metadata, response) { 423 var body = "012345678901234567890123456789"; 424 response.seizePower(); 425 response.write("HTTP/1.0 200 OK\r\n"); 426 response.write("Content-Type: text/plain\r\n"); 427 response.write("Content-Length: 30\r\n"); 428 response.write("Content-Length: -1\r\n"); 429 response.write("Connection: close\r\n"); 430 response.write("\r\n"); 431 response.write(body); 432 response.finish(); 433 } 434 435 function completeTest15(request) { 436 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 437 438 run_test_number(16); 439 } 440 441 //////////////////////////////////////////////////////////////////////////////// 442 // empty content length is ok 443 test_flags[16] = CL_ALLOW_UNKNOWN_CL; 444 let reran16 = false; 445 446 function handler16(metadata, response) { 447 var body = "012345678901234567890123456789"; 448 response.seizePower(); 449 response.write("HTTP/1.0 200 OK\r\n"); 450 response.write("Content-Type: text/plain\r\n"); 451 response.write("Content-Length: \r\n"); 452 response.write("Cache-Control: max-age=600\r\n"); 453 response.write("Connection: close\r\n"); 454 response.write("\r\n"); 455 response.write(body); 456 response.finish(); 457 } 458 459 function completeTest16(request, data) { 460 Assert.equal(request.status, Cr.NS_OK); 461 Assert.equal(30, data.length); 462 463 if (!reran16) { 464 reran16 = true; 465 run_test_number(16); 466 } else { 467 run_test_number(17); 468 } 469 } 470 471 //////////////////////////////////////////////////////////////////////////////// 472 // empty content length paired with non empty is not ok 473 test_flags[17] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; 474 475 function handler17(metadata, response) { 476 var body = "012345678901234567890123456789"; 477 response.seizePower(); 478 response.write("HTTP/1.0 200 OK\r\n"); 479 response.write("Content-Type: text/plain\r\n"); 480 response.write("Content-Length: \r\n"); 481 response.write("Content-Length: 30\r\n"); 482 response.write("Connection: close\r\n"); 483 response.write("\r\n"); 484 response.write(body); 485 response.finish(); 486 } 487 488 function completeTest17(request) { 489 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 490 491 run_test_number(18); 492 } 493 494 //////////////////////////////////////////////////////////////////////////////// 495 // alpha content-length is just like -1 496 test_flags[18] = CL_ALLOW_UNKNOWN_CL; 497 498 function handler18(metadata, response) { 499 var body = "012345678901234567890123456789"; 500 response.seizePower(); 501 response.write("HTTP/1.0 200 OK\r\n"); 502 response.write("Content-Type: text/plain\r\n"); 503 response.write("Content-Length: seventeen\r\n"); 504 response.write("Connection: close\r\n"); 505 response.write("\r\n"); 506 response.write(body); 507 response.finish(); 508 } 509 510 function completeTest18(request, data) { 511 Assert.equal(request.status, Cr.NS_OK); 512 Assert.equal(30, data.length); 513 514 run_test_number(19); 515 } 516 517 //////////////////////////////////////////////////////////////////////////////// 518 // semi-colons are ok too in the content-length 519 test_flags[19] = CL_ALLOW_UNKNOWN_CL; 520 521 function handler19(metadata, response) { 522 var body = "012345678901234567890123456789"; 523 response.seizePower(); 524 response.write("HTTP/1.0 200 OK\r\n"); 525 response.write("Content-Type: text/plain\r\n"); 526 response.write("Content-Length: 30;\r\n"); 527 response.write("Connection: close\r\n"); 528 response.write("\r\n"); 529 response.write(body); 530 response.finish(); 531 } 532 533 function completeTest19(request, data) { 534 Assert.equal(request.status, Cr.NS_OK); 535 Assert.equal(30, data.length); 536 537 run_test_number(20); 538 } 539 540 //////////////////////////////////////////////////////////////////////////////// 541 // FAIL if 1st Location: header is blank, followed by non-blank 542 test_flags[20] = CL_EXPECT_FAILURE; 543 544 function handler20(metadata, response) { 545 var body = "012345678901234567890123456789"; 546 response.seizePower(); 547 response.write("HTTP/1.0 301 Moved\r\n"); 548 response.write("Content-Type: text/plain\r\n"); 549 response.write("Content-Length: 30\r\n"); 550 // redirect to previous test handler that completes OK: test 4 551 response.write("Location:\r\n"); 552 response.write("Location: " + URL + testPathBase + "4\r\n"); 553 response.write("Connection: close\r\n"); 554 response.write("\r\n"); 555 response.write(body); 556 response.finish(); 557 } 558 559 function completeTest20(request) { 560 Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); 561 562 endTests(); 563 }