Verified Commit 099112ed authored by ma1's avatar ma1 Committed by Pier Angelo Vendrame
Browse files

Bug 42019: Empty browser's clipboard on browser shutdown

parent 49c20556
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -87,6 +87,9 @@ pref("browser.sessionstore.resume_from_crash", false);
// Also not needed in PBM at the moment.
pref("browser.pagethumbnails.capturing_disabled", true);

// Empty clipboard content from private windows on exit (tor-browser#42154)
pref("browser.privatebrowsing.preserveClipboard", false);

// Enable HTTPS-Only mode (tor-browser#19850)
pref("dom.security.https_only_mode", true);
// The previous pref automatically sets this to true (see StaticPrefList.yaml),
+154 −0
Original line number Diff line number Diff line
@@ -154,6 +154,157 @@ const PRIVATE_BROWSING_EXE_ICON_INDEX = 1;
const PREF_PRIVATE_BROWSING_SHORTCUT_CREATED =
  "browser.privacySegmentation.createdShortcut";

// Empty clipboard content from private windows on exit
// (tor-browser#42154)
const ClipboardPrivacy = {
  _lastClipboardHash: null,
  _globalActivation: false,
  _isPrivateClipboard: false,
  _hasher: null,
  _shuttingDown: false,

  _createTransferable() {
    const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
      Ci.nsITransferable
    );
    trans.init(null);
    return trans;
  },
  _computeClipboardHash() {
    const trans = this._createTransferable();
    ["text/x-moz-url", "text/plain"].forEach(trans.addDataFlavor);
    try {
      Services.clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
      const clipboardContent = {};
      trans.getAnyTransferData({}, clipboardContent);
      const { data } = clipboardContent.value.QueryInterface(
        Ci.nsISupportsString
      );
      const bytes = new TextEncoder().encode(data);
      const hasher = (this._hasher ||= Cc[
        "@mozilla.org/security/hash;1"
      ].createInstance(Ci.nsICryptoHash));
      hasher.init(hasher.SHA256);
      hasher.update(bytes, bytes.length);
      return hasher.finish(true);
    } catch (e) {}
    return null;
  },

  startup() {
    this._lastClipboardHash = this._computeClipboardHash();

    // Here we track changes in active window / application,
    // by filtering focus events and window closures.
    const handleActivation = (win, activation) => {
      if (activation) {
        if (!this._globalActivation) {
          // focus changed within this window, bail out.
          return;
        }
        this._globalActivation = false;
      } else if (!Services.focus.activeWindow) {
        // focus is leaving this window:
        // let's track whether it remains within the browser.
        lazy.setTimeout(() => {
          this._globalActivation = !Services.focus.activeWindow;
        }, 100);
      }

      const checkClipboardContent = () => {
        const clipboardHash = this._computeClipboardHash();
        if (clipboardHash !== this._lastClipboardHash) {
          this._isPrivateClipboard =
            !activation &&
            (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
              lazy.PrivateBrowsingUtils.isWindowPrivate(win));
          this._lastClipboardHash = clipboardHash;
          console.log(
            `Clipboard changed: private ${this._isPrivateClipboard}, hash ${clipboardHash}.`
          );
        }
      };

      if (win.closed) {
        checkClipboardContent();
      } else {
        // defer clipboard access on DOM events to work-around tor-browser#42306
        lazy.setTimeout(checkClipboardContent, 0);
      }
    };
    const focusListener = e =>
      e.isTrusted && handleActivation(e.currentTarget, e.type === "focusin");
    const initWindow = win => {
      for (const e of ["focusin", "focusout"]) {
        win.addEventListener(e, focusListener);
      }
    };
    for (const w of Services.ww.getWindowEnumerator()) {
      initWindow(w);
    }
    Services.ww.registerNotification((win, event) => {
      switch (event) {
        case "domwindowopened":
          initWindow(win);
          break;
        case "domwindowclosed":
          handleActivation(win, false);
          if (
            this._isPrivateClipboard &&
            lazy.PrivateBrowsingUtils.isWindowPrivate(win) &&
            (this._shuttingDown ||
              !Array.from(Services.ww.getWindowEnumerator()).find(
                w =>
                  lazy.PrivateBrowsingUtils.isWindowPrivate(w) &&
                  // We need to filter out the HIDDEN WebExtensions window,
                  // which might be private as well but is not UI-relevant.
                  !w.location.href.startsWith("chrome://extensions/")
              ))
          ) {
            // no more private windows, empty private content if needed
            this.emptyPrivate();
          }
      }
    });

    lazy.AsyncShutdown.quitApplicationGranted.addBlocker(
      "ClipboardPrivacy: removing private data",
      () => {
        this._shuttingDown = true;
        this.emptyPrivate();
      }
    );
  },
  emptyPrivate() {
    if (
      this._isPrivateClipboard &&
      !Services.prefs.getBoolPref(
        "browser.privatebrowsing.preserveClipboard",
        false
      ) &&
      this._lastClipboardHash === this._computeClipboardHash()
    ) {
      // nsIClipboard.emptyClipboard() does nothing in Wayland:
      // we'll set an empty string as a work-around.
      const trans = this._createTransferable();
      const flavor = "text/plain";
      trans.addDataFlavor(flavor);
      const emptyString = Cc["@mozilla.org/supports-string;1"].createInstance(
        Ci.nsISupportsString
      );
      emptyString.data = "";
      trans.setTransferData(flavor, emptyString);
      const { clipboard } = Services,
        { kGlobalClipboard } = clipboard;
      clipboard.setData(trans, null, kGlobalClipboard);
      clipboard.emptyClipboard(kGlobalClipboard);
      this._lastClipboardHash = null;
      this._isPrivateClipboard = false;
      console.log("Private clipboard emptied.");
    }
  },
};

/**
 * Fission-compatible JSProcess implementations.
 * Each actor options object takes the form of a ProcessActorOptions dictionary.
@@ -1725,6 +1876,8 @@ BrowserGlue.prototype = {

    lazy.DoHController.init();

    ClipboardPrivacy.startup();

    this._firstWindowTelemetry(aWindow);
    this._firstWindowLoaded();

@@ -1986,6 +2139,7 @@ BrowserGlue.prototype = {
          lazy.UpdateListener.reset();
        }
      },
      () => ClipboardPrivacy.emptyPrivate(), // tor-browser#42019
    ];

    for (let task of tasks) {