Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- Manifest: netwerk/cookie/test/unit/xpcshell.toml
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
const { NetUtil } = ChromeUtils.importESModule(
  "resource://gre/modules/NetUtil.sys.mjs"
);
registerCleanupFunction(() => {
  Services.prefs.clearUserPref("network.cookie.cookieBehavior");
  Services.prefs.clearUserPref(
    "network.cookieJarSettings.unblocked_for_testing"
  );
  Services.prefs.clearUserPref("network.cookie.CHIPS.enabled");
  Services.prefs.clearUserPref("network.cookie.chips.partitionLimitEnabled");
  Services.prefs.clearUserPref(
    "network.cookie.chips.partitionLimitByteCapacity"
  );
  Services.prefs.clearUserPref("network.cookie.chips.partitionLimitDryRun");
  Services.cookies.removeAll();
});
// enable chips and chips partition limit
add_setup(async () => {
  Services.prefs.setIntPref("network.cookie.cookieBehavior", 5);
  Services.prefs.setBoolPref(
    "network.cookieJarSettings.unblocked_for_testing",
    true
  );
  Services.prefs.setBoolPref("network.cookie.CHIPS.enabled", true);
  Services.prefs.setBoolPref(
    "network.cookie.chips.partitionLimitEnabled",
    true
  );
  Services.prefs.setBoolPref(
    "network.cookie.chips.partitionLimitDryRun",
    false
  );
  // FOG needs a profile directory to put its data in.
  do_get_profile();
  // FOG needs to be initialized in order for data to flow.
  Services.fog.initializeFOG();
});
function headerify(cookie, index, partitioned) {
  let maxAge = 9000 + index; // use index so cookies can be ordered by age
  // there is no way to test insecure purging first with a partitioned cookie without `Secure`
  // these cookies would be immediately rejected
  let mostHeaders = `; Max-Age=${maxAge}; SameSite=None; Secure`;
  let temp = cookie.concat(mostHeaders);
  if (partitioned) {
    temp = temp.concat("; Partitioned");
  }
  return temp;
}
async function checkReportedOverflow(expected) {
  let reported =
    await Glean.networking.cookieChipsPartitionLimitOverflow.testGetValue();
  if (expected == 0) {
    Assert.equal(reported, null);
    return;
  }
  // verify the telemetry by summing, but the individual reports are not aggregated
  Assert.equal(reported.sum, expected);
}
function channelMaybePartitioned(uri, partition) {
  let channel = NetUtil.newChannel({
    uri,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
    securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
    loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
  });
  if (partition) {
    let cookieJarSettings = Cc[
      "@mozilla.org/cookieJarSettings;1"
    ].createInstance(Ci.nsICookieJarSettings);
    cookieJarSettings.initWithURI(uri, false);
    channel.loadInfo.cookieJarSettings = cookieJarSettings;
  }
  return channel;
}
const BYTE_LIMIT = 10240;
const BYTE_LIMIT_WITH_BUFFER = BYTE_LIMIT * 1.2; // 12288
const BYTES_PER_COOKIE = 100;
const COOKIES_TO_SET_COUNT = 122;
const COOKIES_TO_SET_BYTES = COOKIES_TO_SET_COUNT * BYTES_PER_COOKIE; // 12200
// set many cookies at 100 Bytes each
// partition maximum is 10KiB or 10240B (only for partitioned cookies)
// so after this function is called any partitioned cookie (100B) will exceed
function setManyCookies(uri, channel, partitioned) {
  let cookieString = "";
  let cookieNames = [];
  for (let i = 0; i < COOKIES_TO_SET_COUNT; i++) {
    let name = "c" + i.toString();
    let value =
      i + "_".repeat(BYTES_PER_COOKIE - i.toString().length - name.length);
    let cookie = name + "=" + value;
    cookieNames.push(name);
    Services.cookies.setCookieStringFromHttp(
      uri,
      headerify(cookie, i, partitioned),
      channel
    );
    // prep the expected value
    cookieString += cookie;
    if (i < COOKIES_TO_SET_COUNT - 1) {
      cookieString += "; ";
    }
  }
  return { cookieString, cookieNames };
}
// unpartitioned cookies should not be purged
add_task(async function test_chips_limit_parent_http_unpartitioned() {
  let baseDomain = "example.org";
  let uri = NetUtil.newURI("https://" + baseDomain + "/");
  let channel = channelMaybePartitioned(uri, baseDomain, false);
  let expected = setManyCookies(uri, channel, false);
  expected.cookieNames.push("exceeded");
  // pre-condition: check that all got added as expected
  let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
  Assert.equal(actual, expected.cookieString);
  await checkReportedOverflow(0);
  // unpartitioned cookies, no limit here
  let cookie = "exceeded".concat("=").concat("x".repeat(240));
  Services.cookies.setCookieStringFromHttp(
    uri,
    headerify(cookie, COOKIES_TO_SET_COUNT, false), // use count for uniqueness
    channel
  );
  await checkReportedOverflow(0);
  // extract cookie names from string and compare to expected values
  let second = Services.cookies.getCookieStringFromHttp(uri, channel);
  let cookies = second.split("; ");
  for (let i = 0; i < cookies.length; i++) {
    cookies[i] = cookies[i].substr(0, cookies[i].indexOf("="));
  }
  Assert.deepEqual(cookies, expected.cookieNames);
  Assert.equal(cookies.length, expected.cookieNames.length);
  Services.cookies.removeAll();
  Services.fog.testResetFOG();
});
// parent http partition cookies exceeding capacity should purge in FIFO manner
add_task(async function test_chips_limit_parent_http_partitioned() {
  let baseDomain = "example.org";
  let uri = NetUtil.newURI("https://" + baseDomain + "/");
  let channel = channelMaybePartitioned(uri, baseDomain, true);
  let expected = setManyCookies(uri, channel, true);
  expected.cookieNames.push("exceeded");
  // with the buffer quite a few cookies will be purged
  for (let i = 0; i < 23; i++) {
    expected.cookieNames.shift();
  }
  // pre-condition: check that all got added as expected
  let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
  Assert.equal(actual, expected.cookieString);
  await checkReportedOverflow(0); // no reporting until over the hard cap
  // adding 248 Bytes has excess of 208Bytes (3 cookies will be purged (FIFO))
  let cookie = "exceeded".concat("=").concat("x".repeat(240));
  let cookieNameValueLen = cookie.length - 1;
  Services.cookies.setCookieStringFromHttp(
    uri,
    headerify(cookie, COOKIES_TO_SET_COUNT, true), // use count for uniqueness
    channel
  );
  let expectedOverflow =
    COOKIES_TO_SET_BYTES + cookieNameValueLen - BYTE_LIMIT_WITH_BUFFER;
  await checkReportedOverflow(expectedOverflow);
  // extract cookie names from string and compare to expected values
  let second = Services.cookies.getCookieStringFromHttp(uri, channel);
  let cookies = second.split("; ");
  for (let i = 0; i < cookies.length; i++) {
    cookies[i] = cookies[i].substr(0, cookies[i].indexOf("="));
  }
  Assert.deepEqual(cookies, expected.cookieNames);
  Assert.equal(cookies.length, expected.cookieNames.length);
  Services.cookies.removeAll();
  Services.fog.testResetFOG();
});
// partition limit should still work for cookie overwrites
add_task(async function test_chips_limit_overwrites_can_purge() {
  let baseDomain = "example.org";
  let uri = NetUtil.newURI("https://" + baseDomain + "/");
  let channel = channelMaybePartitioned(uri, baseDomain, true);
  let expected = setManyCookies(uri, channel, true);
  for (let i = 0; i < 22; i++) {
    expected.cookieNames.shift();
  }
  // pre-condition: check that all got added as expected
  let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
  Assert.equal(actual, expected.cookieString);
  await checkReportedOverflow(0);
  // cookie which already exists also triggers purge
  // 244 (new cookie) - 100 (existing cookie) -> 144 newly added bytes
  // So we are in excess by 104 bytes, means 2 cookies need purging (FIFO)
  let cookie = "c101".concat("=").concat("x".repeat(240)); // 244
  let cookieNameValueLen = cookie.length - 1;
  Services.cookies.setCookieStringFromHttp(
    uri,
    headerify(cookie, COOKIES_TO_SET_COUNT, true), // use count for uniqueness
    channel
  );
  let expectedOverflow =
    COOKIES_TO_SET_BYTES +
    cookieNameValueLen -
    BYTES_PER_COOKIE -
    BYTE_LIMIT_WITH_BUFFER;
  await checkReportedOverflow(expectedOverflow);
  // extract cookie names from string and compare to expected values
  let second = Services.cookies.getCookieStringFromHttp(uri, channel);
  let cookies = second.split("; ");
  for (let i = 0; i < cookies.length; i++) {
    cookies[i] = cookies[i].substr(0, cookies[i].indexOf("="));
  }
  Assert.deepEqual(cookies, expected.cookieNames);
  Assert.equal(cookies.length, expected.cookieNames.length);
  Services.cookies.removeAll();
  Services.fog.testResetFOG();
});
// dry run mode should not purge, but still report excess via telemetry
add_task(async function test_chips_limit_dry_run_no_purge() {
  Services.prefs.setBoolPref("network.cookie.chips.partitionLimitDryRun", true);
  let baseDomain = "example.org";
  let uri = NetUtil.newURI("https://" + baseDomain + "/");
  let channel = channelMaybePartitioned(uri, baseDomain, true);
  let expected = setManyCookies(uri, channel, true);
  expected.cookieNames.push("exceeded");
  // pre-condition: check that all got added as expected
  let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
  Assert.equal(actual, expected.cookieString);
  await checkReportedOverflow(0);
  // adding 248 Bytes has excess of 208Bytes (3 cookies will be purged (FIFO))
  let cookie = "exceeded".concat("=").concat("x".repeat(240));
  let cookieNameValueLen = cookie.length - 1;
  Services.cookies.setCookieStringFromHttp(
    uri,
    headerify(cookie, COOKIES_TO_SET_COUNT, true), // use count for uniqueness
    channel
  );
  expected.cookieString += "; ".concat(cookie);
  let expectedOverflow =
    COOKIES_TO_SET_BYTES + cookieNameValueLen - BYTE_LIMIT_WITH_BUFFER;
  await checkReportedOverflow(expectedOverflow);
  // extract cookie names from string and compare to expected values
  let second = Services.cookies.getCookieStringFromHttp(uri, channel);
  let cookies = second.split("; ");
  for (let i = 0; i < cookies.length; i++) {
    cookies[i] = cookies[i].substr(0, cookies[i].indexOf("="));
  }
  Assert.deepEqual(cookies, expected.cookieNames);
  Assert.equal(cookies.length, expected.cookieNames.length);
  Assert.equal(second, expected.cookieString);
  Services.cookies.removeAll();
  Services.fog.testResetFOG();
});
add_task(async function test_chips_limit_chips_off() {
  Services.prefs.setBoolPref(
    "network.cookie.chips.partitionLimitDryRun",
    false
  );
  Services.prefs.setBoolPref("network.cookie.CHIPS.enabled", false);
  let baseDomain = "example.org";
  let uri = NetUtil.newURI("https://" + baseDomain + "/");
  let channel = channelMaybePartitioned(uri, baseDomain, true);
  let expected = setManyCookies(uri, channel, true);
  expected.cookieNames.push("exceeded");
  // pre-condition: check that all got added as expected
  let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
  Assert.equal(actual, expected.cookieString);
  await checkReportedOverflow(0);
  // shouldn't trigger purge when CHIPS disabled
  let cookie = "exceeded".concat("=").concat("x".repeat(240));
  Services.cookies.setCookieStringFromHttp(
    uri,
    headerify(cookie, COOKIES_TO_SET_COUNT, true), // use count for uniqueness
    channel
  );
  expected.cookieString += "; ".concat(cookie);
  await checkReportedOverflow(0);
  // extract cookie names from string and compare to expected values
  let second = Services.cookies.getCookieStringFromHttp(uri, channel);
  let cookies = second.split("; ");
  for (let i = 0; i < cookies.length; i++) {
    cookies[i] = cookies[i].substr(0, cookies[i].indexOf("="));
  }
  Assert.deepEqual(cookies, expected.cookieNames);
  Assert.equal(cookies.length, expected.cookieNames.length);
  Assert.equal(second, expected.cookieString);
  Services.cookies.removeAll();
  Services.fog.testResetFOG();
});
add_task(async function test_chips_limit_chips_limit_off() {
  Services.prefs.setBoolPref(
    "network.cookie.chips.partitionLimitDryRun",
    false
  );
  Services.prefs.setBoolPref("network.cookie.CHIPS.enabled", true);
  Services.prefs.setBoolPref(
    "network.cookie.chips.partitionLimitEnabled",
    false
  );
  let baseDomain = "example.org";
  let uri = NetUtil.newURI("https://" + baseDomain + "/");
  let channel = channelMaybePartitioned(uri, baseDomain, true);
  let expected = setManyCookies(uri, channel, true);
  expected.cookieNames.push("exceeded");
  // pre-condition: check that all got added as expected
  let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
  Assert.equal(actual, expected.cookieString);
  await checkReportedOverflow(0);
  // shouldn't trigger purge when CHIPS limit disabled
  let cookie = "exceeded".concat("=").concat("x".repeat(240));
  Services.cookies.setCookieStringFromHttp(
    uri,
    headerify(cookie, COOKIES_TO_SET_COUNT, true), // use count for uniqueness
    channel
  );
  expected.cookieString += "; ".concat(cookie);
  await checkReportedOverflow(0);
  // extract cookie names from string and compare to expected values
  let second = Services.cookies.getCookieStringFromHttp(uri, channel);
  let cookies = second.split("; ");
  for (let i = 0; i < cookies.length; i++) {
    cookies[i] = cookies[i].substr(0, cookies[i].indexOf("="));
  }
  Assert.deepEqual(cookies, expected.cookieNames);
  Assert.equal(cookies.length, expected.cookieNames.length);
  Assert.equal(second, expected.cookieString);
  Services.cookies.removeAll();
  Services.fog.testResetFOG();
});
// non-chips-partitioned cookies do not trigger the limit
add_task(async function test_chips_limit_non_chips_partitioned() {
  // channel is partitioned, not cookie header -> non-chips-partition cookies
  let baseDomain = "example.org";
  let uri = NetUtil.newURI("https://" + baseDomain + "/");
  let channel = channelMaybePartitioned(uri, baseDomain, true);
  let expected = setManyCookies(uri, channel, false);
  expected.cookieNames.push("exceeded");
  // pre-condition: check that all got added as expected
  let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
  Assert.equal(actual, expected.cookieString);
  await checkReportedOverflow(0);
  // non-chips-partitioned cookies, should also have no limit
  let cookie = "exceeded".concat("=").concat("x".repeat(240));
  Services.cookies.setCookieStringFromHttp(
    uri,
    headerify(cookie, COOKIES_TO_SET_COUNT, false), // use count for uniqueness
    channel
  );
  await checkReportedOverflow(0);
  // extract cookie names from string and compare to expected values
  let second = Services.cookies.getCookieStringFromHttp(uri, channel);
  let cookies = second.split("; ");
  for (let i = 0; i < cookies.length; i++) {
    cookies[i] = cookies[i].substr(0, cookies[i].indexOf("="));
  }
  Assert.deepEqual(cookies, expected.cookieNames);
  Assert.equal(cookies.length, expected.cookieNames.length);
  Services.cookies.removeAll();
  Services.fog.testResetFOG();
});