/* * Tests bugs 597706, 655389: prevent duplicate headers with differing values * for some headers like Content-Length, Location, etc. */ //////////////////////////////////////////////////////////////////////////////// // Test infrastructure "use strict"; // The tests in this file use number indexes to run, which can't be detected // via ESLint. /* eslint-disable no-unused-vars */ const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); XPCOMUtils.defineLazyGetter(this, "URL", function() { return "http://localhost:" + httpserver.identity.primaryPort; }); var httpserver = new HttpServer(); var test_flags = []; var testPathBase = "/dupe_hdrs"; function run_test() { httpserver.start(-1); do_test_pending(); run_test_number(1); } function run_test_number(num) { let testPath = testPathBase + num; httpserver.registerPathHandler(testPath, globalThis["handler" + num]); var channel = setupChannel(testPath); let flags = test_flags[num]; // OK if flags undefined for test channel.asyncOpen( new ChannelListener(globalThis["completeTest" + num], channel, flags) ); } function setupChannel(url) { var chan = NetUtil.newChannel({ uri: URL + url, loadUsingSystemPrincipal: true, }); var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); return httpChan; } function endTests() { httpserver.stop(do_test_finished); } //////////////////////////////////////////////////////////////////////////////// // Test 1: FAIL because of conflicting Content-Length headers test_flags[1] = CL_EXPECT_FAILURE; function handler1(metadata, response) { var body = "012345678901234567890123456789"; // Comrades! We must seize power from the petty-bourgeois running dogs of // httpd.js in order to reply with multiple instances of the same header! response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Content-Length: 20\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest1(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(2); } //////////////////////////////////////////////////////////////////////////////// // Test 2: OK to have duplicate same Content-Length headers function handler2(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Content-Length: 30\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest2(request, data, ctx) { Assert.equal(request.status, 0); run_test_number(3); } //////////////////////////////////////////////////////////////////////////////// // Test 3: FAIL: 2nd Content-length is blank test_flags[3] = CL_EXPECT_FAILURE; function handler3(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Content-Length:\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest3(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(4); } //////////////////////////////////////////////////////////////////////////////// // Test 4: ensure that blank C-len header doesn't allow attacker to reset Clen, // then insert CRLF attack test_flags[4] = CL_EXPECT_FAILURE; function handler4(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); // Bad Mr Hacker! Bad! var evilBody = "We are the Evil bytes, Evil bytes, Evil bytes!"; response.write("Content-Length:\r\n"); response.write("Content-Length: %s\r\n\r\n%s" % (evilBody.length, evilBody)); response.write("\r\n"); response.write(body); response.finish(); } function completeTest4(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(5); } //////////////////////////////////////////////////////////////////////////////// // Test 5: ensure that we take 1st instance of duplicate, nonmerged headers that // are permitted : (ex: Referrer) function handler5(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Referer: naive.org\r\n"); response.write("Referer: evil.net\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest5(request, data, ctx) { try { let referer = request.getResponseHeader("Referer"); Assert.equal(referer, "naive.org"); } catch (ex) { do_throw("Referer header should be present"); } run_test_number(6); } //////////////////////////////////////////////////////////////////////////////// // Test 5: FAIL if multiple, different Location: headers present // - needed to prevent CRLF injection attacks test_flags[6] = CL_EXPECT_FAILURE; function handler6(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 301 Moved\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Location: " + URL + "/content\r\n"); response.write("Location: http://www.microsoft.com/\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest6(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); // run_test_number(7); // Test 7 leaking under e10s: unrelated bug? run_test_number(8); } //////////////////////////////////////////////////////////////////////////////// // Test 7: OK to have multiple Location: headers with same value function handler7(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 301 Moved\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); // redirect to previous test handler that completes OK: test 5 response.write("Location: " + URL + testPathBase + "5\r\n"); response.write("Location: " + URL + testPathBase + "5\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest7(request, data, ctx) { // for some reason need this here request.QueryInterface(Ci.nsIHttpChannel); try { let referer = request.getResponseHeader("Referer"); Assert.equal(referer, "naive.org"); } catch (ex) { do_throw("Referer header should be present"); } run_test_number(8); } //////////////////////////////////////////////////////////////////////////////// // FAIL if 2nd Location: headers blank test_flags[8] = CL_EXPECT_FAILURE; function handler8(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 301 Moved\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); // redirect to previous test handler that completes OK: test 4 response.write("Location: " + URL + testPathBase + "4\r\n"); response.write("Location:\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest8(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(9); } //////////////////////////////////////////////////////////////////////////////// // Test 9: ensure that blank Location header doesn't allow attacker to reset, // then insert an evil one test_flags[9] = CL_EXPECT_FAILURE; function handler9(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 301 Moved\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); // redirect to previous test handler that completes OK: test 2 response.write("Location: " + URL + testPathBase + "2\r\n"); response.write("Location:\r\n"); // redirect to previous test handler that completes OK: test 4 response.write("Location: " + URL + testPathBase + "4\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest9(request, data, ctx) { // All redirection should fail: Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(10); } //////////////////////////////////////////////////////////////////////////////// // Test 10: FAIL: if conflicting values for Content-Dispo test_flags[10] = CL_EXPECT_FAILURE; function handler10(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Content-Disposition: attachment; filename=foo\r\n"); response.write("Content-Disposition: attachment; filename=bar\r\n"); response.write("Content-Disposition: attachment; filename=baz\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest10(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(11); } //////////////////////////////////////////////////////////////////////////////// // Test 11: OK to have duplicate same Content-Disposition headers function handler11(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Content-Disposition: attachment; filename=foo\r\n"); response.write("Content-Disposition: attachment; filename=foo\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest11(request, data, ctx) { Assert.equal(request.status, 0); try { var chan = request.QueryInterface(Ci.nsIChannel); Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); Assert.equal(chan.contentDispositionFilename, "foo"); Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo"); } catch (ex) { do_throw("error parsing Content-Disposition: " + ex); } run_test_number(12); } //////////////////////////////////////////////////////////////////////////////// // Bug 716801 OK for Location: header to be blank function handler12(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Location:\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest12(request, data, ctx) { Assert.equal(request.status, Cr.NS_OK); Assert.equal(30, data.length); run_test_number(13); } //////////////////////////////////////////////////////////////////////////////// // Negative content length is ok test_flags[13] = CL_ALLOW_UNKNOWN_CL; function handler13(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: -1\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest13(request, data, ctx) { Assert.equal(request.status, Cr.NS_OK); Assert.equal(30, data.length); run_test_number(14); } //////////////////////////////////////////////////////////////////////////////// // leading negative content length is not ok if paired with positive one test_flags[14] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; function handler14(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: -1\r\n"); response.write("Content-Length: 30\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest14(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(15); } //////////////////////////////////////////////////////////////////////////////// // trailing negative content length is not ok if paired with positive one test_flags[15] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; function handler15(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); response.write("Content-Length: -1\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest15(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(16); } //////////////////////////////////////////////////////////////////////////////// // empty content length is ok test_flags[16] = CL_ALLOW_UNKNOWN_CL; let reran16 = false; function handler16(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: \r\n"); response.write("Cache-Control: max-age=600\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest16(request, data, ctx) { Assert.equal(request.status, Cr.NS_OK); Assert.equal(30, data.length); if (!reran16) { reran16 = true; run_test_number(16); } else { run_test_number(17); } } //////////////////////////////////////////////////////////////////////////////// // empty content length paired with non empty is not ok test_flags[17] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; function handler17(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: \r\n"); response.write("Content-Length: 30\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest17(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); run_test_number(18); } //////////////////////////////////////////////////////////////////////////////// // alpha content-length is just like -1 test_flags[18] = CL_ALLOW_UNKNOWN_CL; function handler18(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: seventeen\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest18(request, data, ctx) { Assert.equal(request.status, Cr.NS_OK); Assert.equal(30, data.length); run_test_number(19); } //////////////////////////////////////////////////////////////////////////////// // semi-colons are ok too in the content-length test_flags[19] = CL_ALLOW_UNKNOWN_CL; function handler19(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 200 OK\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30;\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest19(request, data, ctx) { Assert.equal(request.status, Cr.NS_OK); Assert.equal(30, data.length); run_test_number(20); } //////////////////////////////////////////////////////////////////////////////// // FAIL if 1st Location: header is blank, followed by non-blank test_flags[20] = CL_EXPECT_FAILURE; function handler20(metadata, response) { var body = "012345678901234567890123456789"; response.seizePower(); response.write("HTTP/1.0 301 Moved\r\n"); response.write("Content-Type: text/plain\r\n"); response.write("Content-Length: 30\r\n"); // redirect to previous test handler that completes OK: test 4 response.write("Location:\r\n"); response.write("Location: " + URL + testPathBase + "4\r\n"); response.write("Connection: close\r\n"); response.write("\r\n"); response.write(body); response.finish(); } function completeTest20(request, data, ctx) { Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); endTests(); }