toolkit/components/passwordmgr/test/mochitest/test_munged_values.html
author Masatoshi Kimura <VYV03354@nifty.ne.jp>
Fri, 22 May 2020 21:46:25 +0000
changeset 531728 a845717e4d10ad7cb65ca64b08666c373db6e1de
parent 524646 27e3764214013f4dd7b18cf845a6195462c8a830
child 531733 d5c77659d412d28268519c1add754776fa566c1c
permissions -rw-r--r--
Bug 1482279 - Stop using Cu.forcePermissiveCOWs() in SpecialPowers. r=kmag Differential Revision: https://phabricator.services.mozilla.com/D74641

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Test handling of possibly-manipulated username values</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script>
  <script src="pwmgr_common.js"></script>
  <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script type="application/javascript">
let readyPromise = registerRunTests();

function add2logins() {
  runInParent(function initLogins() {
    const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
    Services.logins.removeAllLogins();

    let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
    login1.init("https://example.org", "https://example.org", null, "real••••user", "pass1", "", "");

    let login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
    login2.init("https://example.org", "https://example.org", null, "user2", "pass2", "", "");

    Services.logins.addLogin(login1);
    Services.logins.addLogin(login2);
  });
}

function addSingleLogin() {
  runInParent(function initWithSingleLogin() {
    const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
    Services.logins.removeAllLogins();

    let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
    login1.init("https://example.org", "https://example.org", null, "real••••user", "pass1", "", "");
    Services.logins.addLogin(login1);
  });
}

/**
 *  For any test including the character "!", generate a version of that test for every known munge
 *  character.
 **/
 function generateTestCases(test) {
  const MUNGE_CHARS = ["*", ".", "•"];

  let nothingToReplace = Object.values(test).every(value => typeof value !== "string" || !value.includes("!"));
  if (nothingToReplace) {
    return test;
  };

  return MUNGE_CHARS.map(char => {
    let newTest = {};
    for (let [propName, val] of Object.entries(test)) {
      if (typeof val === "string") {
        newTest[propName] = val.replace(/!/g, char);
      } else {
        newTest[propName] = val;
      }
  };
  return newTest;
})};

let loadPromise = new Promise(resolve => {
  document.addEventListener("DOMContentLoaded", () => {
    document.getElementById("loginFrame").addEventListener("load", (evt) => {
      resolve();
    });
  });
});

async function loadFormIntoIframe(origin, html) {
  let loginFrame = document.getElementById("loginFrame");
  let loadedPromise = new Promise((resolve) => {
    loginFrame.addEventListener("load", function() {
      resolve();
    }, {once: true});
  });
  loginFrame.src = origin + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
  await loadedPromise;
  await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [html], function(contentHtml) {
    // eslint-disable-next-line no-unsanitized/property
    this.content.document.documentElement.innerHTML = contentHtml;
  });

  // Wait for the form to be processed before trying to submit.
  await promiseFormsProcessed();

  await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [html], function(contentHtml) {
    let doc = this.content.document;
    for (let field of doc.querySelectorAll("input")) {
      let actualValue = field.value;
      field.value = "";
      SpecialPowers.wrap(field).setUserInput(actualValue);
    }
  });
}

add_task(async function setup() {
  info("Waiting for setup and page and frame loads");
  await readyPromise;
  await loadPromise;
});

const DEFAULT_ORIGIN = "https://example.com";
const ORG_ORIGIN = "https://example.org";

add_task(async function test_new_logins() {
  const TEST_CASES = [
  // ! is replaced with characters commonly used for munging
  {
    testName: "test_middle!MaskedUsername",
    username: "so!!!ne",
    expected: null,
  },
  {
    testName: "test_start!MaskedUsername",
    username: "!!!eone",
    expected: null,
  },
  {
    testName: "test_end!MaskedUsername",
    username: "some!!!",
    expected: null,
  },
  {
    testName: "test_ok!Username",
    username: "obelixand!",
    expected: "obelixand!",
  },
  {
    testName: "test_ok!Username2",
    username: "!!username!!",
    expected: "!!username!!",
  },
  {
    // We should only consider a username munged if it repeats of the same character
    testName: "test_combinedMungeCharacters",
    username: "*.•*.•*.•*.•*.•*.•",
    expected: "*.•*.•*.•*.•*.•*.•",
  },
].flatMap(generateTestCases);

  for (let tc of TEST_CASES) {
    info("Starting testcase: " + JSON.stringify(tc));
    await loadFormIntoIframe(DEFAULT_ORIGIN, `<form id="form1" onsubmit="return false;">
      <input  type="text"       name="uname" value="${tc.username}">
      <input  type="password"   name="pword" value="thepassword">
      <button type="submit" id="submitBtn">Submit</button>
    </form>`);
    await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [tc], function(testcase) {
      let doc = this.content.document;
      Assert.equal(doc.querySelector("[name='uname']").value, testcase.username, "Checking for filled username");
    });

    // Check data sent via PasswordManager:onFormSubmit
    let processedPromise = getSubmitMessage();
    await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
      this.content.document.getElementById("submitBtn").click();
    });

    let submittedResult = await processedPromise;
    info("Got submittedResult: " + JSON.stringify(submittedResult));

    if (tc.expected === null) {
      is(submittedResult.usernameField, tc.expected, "Check usernameField");
    } else {
      is(submittedResult.usernameField.value, tc.expected, "Check usernameField");
    }
  }
});

add_task(async function test_no_save_dialog_when_password_is_fully_munged() {
  const TEST_CASES = [
  {
      testName: "test_passFullyMungedBy!",
      password: "!!!!!!!!!",
      shouldShowPrompt: false,
    },
    {
      testName: "test_passStartsMungedBy!",
      password: "!!!!!!!butThenAPassword",
      shouldShowPrompt: true,
    },
    {
      testName: "test_passEndsMungedBy!",
      password: "aRealPasswordAndThen!!!!!!!",
      shouldShowPrompt: true,
    },
    {
      testName: "test_passMostlyMungedBy!",
      password: "!!!a!!!!",
      shouldShowPrompt: true,
    },
    {
      testName: "test_combinedMungedCharacters",
      password: "*.•*.•*.•*.•",
      shouldShowPrompt: true,
    },
  ].flatMap(generateTestCases);

  for (let tc of TEST_CASES) {
    info("Starting testcase: " + tc.testName)
    await loadFormIntoIframe(DEFAULT_ORIGIN, `<form id="form1" onsubmit="return false;">
      <input  type="text"       name="uname" value="username">
      <input  type="password"   name="pword" value="${tc.password}">
      <button type="submit" id="submitBtn">Submit</button>
    </form>`);

    await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [tc], function(testcase) {
      let doc = this.content.document;
      Assert.equal(doc.querySelector("[name='pword']").value, testcase.password, "Checking for filled password");
    });

    let formSubmitListener = SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
      return new Promise(resolve => {
        this.content.windowRoot.addEventListener(
            "PasswordManager:onFormSubmit",
            event => {
              info(`PasswordManager:onFormSubmit called. Event: ${JSON.stringify(event)}`);
              resolve(event.detail.messageSent);
            }
          );
      });
    });

    await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
      this.content.document.getElementById("submitBtn").click();
    });

    let dialogRequested = await formSubmitListener;

    is(dialogRequested, tc.shouldShowPrompt, "Verify 'show save/update prompt' message sent to parent process");
  }
});

add_task(async function test_no_autofill_munged_username_matching_password() {
  // run this test with 2 matching logins from this origin so we don't autofill
  await add2logins();

  let allLogins = await LoginManager.getAllLogins();
  let matchingLogins = Array.prototype.filter.call(allLogins, l => l.origin == ORG_ORIGIN);
  is(matchingLogins.length, 2, "Expected number of matching logins");

  let bulletLogin = matchingLogins.find(l => l.username == "real••••user");
  ok(bulletLogin, "Found the real••••user login");

  let timesUsed = bulletLogin.timesUsed;
  let guid = bulletLogin.guid;

  await loadFormIntoIframe(ORG_ORIGIN, `<form id="form1" onsubmit="return false;">
    <input  type="text"       name="uname" value="">
    <input  type="password"   name="pword" value="">
    <button type="submit" id="submitBtn">Submit</button>
  </form>`);
  await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
    let doc = this.content.document;
    Assert.equal(doc.querySelector("[name='uname']").value, "", "Check username didn't get autofilled");
    SpecialPowers.wrap(doc.querySelector("[name='uname']")).setUserInput("real••••user");
    SpecialPowers.wrap(doc.querySelector("[name='pword']")).setUserInput("pass1");
  });

  // we shouldn't get the save password doorhanger...
  let popupShownPromise = promiseNoUnexpectedPopupShown();

  // Check data sent via PasswordManager:onFormSubmit
  let processedPromise = getSubmitMessage();
  await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
    this.content.document.getElementById("submitBtn").click();
  });

  let submittedResult = await processedPromise;
  info("Got submittedResult: " + JSON.stringify(submittedResult));

  is(submittedResult.usernameField, null, "Check usernameField");

  let updatedLogins = await LoginManager.getAllLogins();
  let updatedLogin = Array.prototype.find.call(updatedLogins, l => l.guid == guid);
  ok(updatedLogin, "Got the login via guid");
  is(updatedLogin.timesUsed, timesUsed + 1, "timesUsed was incremented");

  await popupShownPromise;
});


add_task(async function test_autofill_munged_username_matching_password() {
  // only a single matching login so we autofill the username
  await addSingleLogin();

  let allLogins = await LoginManager.getAllLogins();
  let matchingLogins = Array.prototype.filter.call(allLogins, l => l.origin == ORG_ORIGIN);
  is(matchingLogins.length, 1, "Expected number of matching logins");

  info("matched login: " + matchingLogins[0].username);
  let bulletLogin = matchingLogins.find(l => l.username == "real••••user");
  ok(bulletLogin, "Found the real••••user login");

  let timesUsed = bulletLogin.timesUsed;
  let guid = bulletLogin.guid;

  await loadFormIntoIframe(ORG_ORIGIN, `<form id="form1" onsubmit="return false;">
    <input  type="text"       name="uname" value="">
    <input  type="password"   name="pword" value="">
    <button type="submit" id="submitBtn">Submit</button>
  </form>`);
  await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
    let doc = this.content.document;
    Assert.equal(doc.querySelector("[name='uname']").value, "real••••user", "Check username did get autofilled");
    doc.querySelector("[name='pword']").setUserInput("pass1");
  });

  // we shouldn't get the save/update password doorhanger as it didn't change
  let popupShownPromise =  promiseNoUnexpectedPopupShown();

  // Check data sent via PasswordManager:onFormSubmit
  let processedPromise = getSubmitMessage();
  await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() {
    this.content.document.getElementById("submitBtn").click();
  });

  let submittedResult = await processedPromise;
  info("Got submittedResult: " + JSON.stringify(submittedResult));

  is(submittedResult.usernameField, null, "Check usernameField");

  let updatedLogins = await LoginManager.getAllLogins();
  let updatedLogin = Array.prototype.find.call(updatedLogins, l => l.guid == guid);
  ok(updatedLogin, "Got the login via guid");
  is(updatedLogin.timesUsed, timesUsed + 1, "timesUsed was incremented");

  await popupShownPromise;
});

</script>

<p id="display"></p>

<div id="content">
  <iframe id="loginFrame" src="https://example.com/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
</div>
<pre id="test"></pre>
</body>
</html>