Skip to main content

Command Palette

Search for a command to run...

Behind Chrome-Based DLP Plugins

How your employers read your browsing history

Updated
16 min read
Behind Chrome-Based DLP Plugins

Cover Illustration by buruberrii_

While we discussed previously about how endpoint-based DLP/EDR agents work in macOS, there is another component of endpoint security systems that are often overlooked and that is the browser extension component. Endpoint Security systems, especially DLPs, use the browser extension component to intercept web traffic and inspect uploaded/downloaded files.

This came into focus unexpectedly with the news that Cyberhaven DLP’s Chrome Extension was compromised by a threat actor over the holidays.

What happened?

On December 24th, 2024, at approximately 5:24 PM UTC, a targeted advanced attack successfully occurred on a Cyberhaven employee. The attacker used the access gained in this attack to publish a malicious Chrome extension (version 24.10.4) to the Chrome Web Store in the early morning of December 25th, 2024.

Cyberhaven's internal security team detected the attack at 11:54 PM UTC on December 25th, 2024. Cyberhaven removed the malicious package within 60 minutes of detection.

What was the impact?

For browsers running the compromised plugin, it is possible for sensitive information, including authenticated sessions and cookies, to be exfiltrated to the attacker's domain (cyberhavenext[.]pro) The exfill domain was online from 1:32AM UTC December 25th, 2024 until 2:50AM UTC on December 26th, 2024 What we recommend on impacted endpoints

Verify that the impacted Cyberhaven Chrome extension version 24.10.4 is updated to 24.10.5 or newer Revoke/rotate all passwords that aren't FIDOv2 Revoke/rotate all API tokens Review all logs to verify no malicious activity Versions not hosted on the Chrome store (Firefox, edge) were not affected Next steps

Cyberhaven will continue its investigation into this incident and update its customers accordingly We are working on providing additional telemetry and additional threat intelligence and will share it with impacted customers as soon as possible Cyberhaven has engaged Mandiant and Federal Law Enforcement to help in this investigation One of Cyberhaven's core values is maximum transparency, and we are acting on these first principles to retain the trust we have earned from you. We will continue to keep you updated and support you in every way possible to mitigate the impact of this incident.

Additional information about the incident:

This incident only impacted machines running Chrome-based browsers that were updated via the Google Chrome Web Store.

After an in-depth review, the only compromise at Cyberhaven was a single admin account for the Google Chrome Store that allowed the attacker to push a malicious Chrome extension and bypass Cyberhaven controls; there was no other attack vector or any additional compromised accounts, including our CI/CD processes or code signing keys. The only impacted version of the plugin is 24.10.4. It only affected machines that were online between 1:32 AM UTC on December 25th, 2024 and 2:50 AM UTC on December 26th, 2024.

We know the attack did not clean up the Chrome data store, so we have included instructions below that your security teams can use to verify what if any, data was exfiltrated. Cyberhaven will be publishing a new Chrome extension (version 24.10.6) that will leverage this new information to gather additional telemetry to narrow down the scope of possible compromised machines; also, this data will allow us to narrow down the scope of possible compromised browsers and understand what if any, data was exfiltrated

Whats a DLP?

Data Loss Prevention (DLP) solutions are security tools that inspect data in-motion (network traffic), at-rest (stored data), and in-use (endpoint actions) to identify and control sensitive data movement based on predefined or custom policies. They operate at network, storage, and OS levels with capabilities for monitoring, blocking, decrypting, or quarantining data that matches sensitive patterns like PII, financial data, or intellectual property.

But why do DLPs need their own Chrome plugin? At a technical level DLPs are similar to EDRs, the main difference is just the ruleset they abide by, so why can’t they just use the agent they already have installed that have root privileges and network monitoring capabilities anyways?

Modern web browsers like Chrome implement a sophisticated security model where each tab and process runs in an isolated sandbox environment. This architectural design prevents traditional OS-level monitoring tools from directly observing or controlling network operations within the browser. The browser's own network stack handles complex protocols, TLS/SSL sessions, connection pooling, and resource prioritization independently of the operating system's network stack.

DLP providers require Chrome extensions because they need privileged access to browser-specific APIs that can intercept and analyze data transfers before they enter the encrypted TLS tunnel. These extensions can tap into chrome.webRequest, chrome.downloads, chrome.tabs, and chrome.storage APIs, providing critical capabilities like pre-TLS content inspection, DOM access for monitoring form uploads, and context awareness to track the exact origin of data transfers. The extension model also gives DLP solutions direct access to browser events and the JavaScript runtime, enabling them to detect and analyze dynamic content uploads through modern web APIs.

While the threat of malicious Chrome extensions have been well known for quite sometime, and sysadmins have mostly migrated to more secure Chrome deployments by limiting extension installation, i always have seen Plugin-based DLP solutions like Cyberhaven, and on this instance Forcepoint, as a weak spot. These are highly privileged addons that are intended to be installed by sysadmins in a wide IT deployment and they’re rarely audited beforehand because of course its a security product, its supposed to be intrusive.

As chrome extensions also operate cross-platform, the material presented here is relevant to both macOS and Windows installations. This post will also focus on a specific chromium extension-based implementation from Forcepoint Endpoint, but i expect other solutions implement similar mechanisms in terms of delivery and interception methods.

Delivery Method

In researching for this issue, i've found this chinese article about how Forcepoint embeds their add-on into Chrome and Firefox which i found unique. It seems like despite being an old dissection of the delivery logic, the implementation remains the same till now.

There is a bash script located in the main installation path for Forcepoint in /Library/Application Support/Websense Endpoint/DLP. This script manages the checking of Chrome's existence and the forceful installation of the add-on.

#!/bin/bash

CUT="/usr/bin/cut"
LSOF="/usr/sbin/lsof"
PS="/bin/ps"
PROFILES="/usr/bin/profiles"
GREP="/usr/bin/grep"

PIDS=`$PS -axc -o pid,command | $GREP "Google Chrome" | $GREP -v "Google Chrome Helper" | $CUT -c 1-5`

LIBWEP_HOOKED=0
if [ ${#PIDS[@]} -gt 0 ]
then
    for pid in $PIDS
    do
        CNT=`"$LSOF" -P -T -p $pid | grep libwep_chrome.dylib | wc -l`
        if [ $CNT == 1 ]
        then
            LIBWEP_HOOKED=1
        fi
    done
fi

if [ $LIBWEP_HOOKED == 1 ]
then
    echo "Configuring the chrome extension profile..."
    $PROFILES -I -F "/Library/Application Support/Websense Endpoint/DLP/WebsenseEndpointExtension.config"
fi

This script (setup_chrome_ext.sh) is designed to check if the Google Chrome browser is running on the system and if a specific library, libwep_chrome.dylib, is loaded by any of the Chrome processes. Then, it uses these utilities to get a list of process IDs (PIDs) for Chrome processes, excluding the "Google Chrome Helper" process.

For each Chrome PID, it checks if the libwep_chrome.dylib library is loaded using the lsof command. If the library is loaded for at least one Chrome process, the script assumes that a specific Chrome extension is installed and proceeds to execute the profiles command with the -I -F options and a configuration file path (/Library/Application Support/Websense Endpoint/DLP/WebsenseEndpointExtension.config).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadIdentifier</key>
<string>com.websense.WebsenseEndpointExtension</string>
<key>PayloadRemovalDisallowed</key>
<false />
<key>PayloadScope</key>
<string>System</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>5A93F4FD-5894-4396-856C-0AD9A0537752</string>
<key>PayloadOrganization</key>
<string>Websense</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadDisplayName</key>
<string>WebsenseEndpoint</string>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.ManagedClient.preferences</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.websense.WebsenseEndpointExtension.E503A43D-BE99-4D26-B9F4-8434364012F7</string>
<key>PayloadUUID</key>
<string>E503A43D-BE99-4D26-B9F4-8434364012F7</string>
<key>PayloadEnabled</key>
<true />
<key>PayloadDisplayName</key>
<string>Google Chrome</string>
<key>PayloadContent</key>
<dict>
<key>com.google.Chrome</key>
<dict>
<key>Forced</key>
<array>
<dict>
<key>mcx_preference_settings</key>
<dict>
<key>ExtensionInstallForcelist</key>
<array>
<string>ljckpacopljdanbdkdddedlackndojmf</string>
</array>
</dict>
</dict>
</array>
</dict>
</dict>
</dict>
</array>
</dict>
</plist>

WebsenseEndpointExtension.config itself is then used to force the installation of the Chrome extension by leveraging the "Managed Client" feature in macOS. Specifically, it uses the com.apple.ManagedClient.preferences payload type to manage the preferences for the Google Chrome application. Within the Chrome preferences, it defines an "ExtensionInstallForcelist" array under the "mcx_preference_settings" key, which contains the extension ID "ljckpacopljdanbdkdddedlackndojmf".

When this configuration profile is deployed and applied to a macOS system, the Managed Client service interprets the forced preferences and instructs Google Chrome to install the specified extension ID from the Chrome Web Store, essentially bypassing the normal user-initiated installation process.

The Extension Itself

The ljckpacopljdanbdkdddedlackndojmf extension is called “Forcepoint Endpoint”. Its around 36 KB and doesn't have alot of content. The main logic of the extension is contained in the background.js file.

At the foundation of the interception system lies the primary request listener that captures all outgoing requests. The extension registers this listener using Chrome's webRequest API:

chrome.webRequest.onBeforeRequest.addListener(function(e) {
    return "POST" != e.method && "PUT" != e.method || (o[e.requestId] = e.requestBody), {
        cancel: false
    }
}, {
    urls: ["<all_urls>"]
}, ["blocking", "requestBody"]);

This listener focuses specifically on POST and PUT requests, which are typically used for file uploads and data submissions. When such a request is detected, the extension stores the request body in a temporary cache (object 'o'). The use of "<all_urls>" as the pattern means the extension monitors all web traffic, not just specific domains or protocols.

The main interception logic occurs in the onBeforeSendHeaders listener. This component performs deep inspection of requests and determines whether they should be allowed or blocked:

chrome.webRequest.onBeforeSendHeaders.addListener(function(r) {
    if (0 == O || "POST" != r.method && "PUT" != r.method) return {
        cancel: false
    };

    if (null != r.url.match(/https?\:\/\/localhost\:55296\/ChromeExt\//i) || 
        null != r.url.match(/mail\.google\.com\/cloudsearch/i)) {
        return { cancel: false }
    }

The extension implements sophisticated request filtering that takes into account various factors. It maintains a cache cleanup mechanism to prevent memory leaks. This cleanup function removes cached requests that are older than 30 seconds, ensuring efficient memory usage during long browsing sessions.

function T() {
    var e, t = (new Date).getTime();
    for (e in D) 3e4 < t - D[e] && delete D[e]
}

The extension performs detailed content analysis, particularly focusing on file uploads and attachments. It includes special handling for various file upload mechanisms. These functions handle different types of file uploads, with special consideration for Google's upload protocol and multipart form data. The boundary detection function is particularly important for parsing multipart form data correctly.

function B(e) {
    for (var t of e)
        if ("x-goog-upload-protocol" == t.name.toLowerCase()) 
            return true;
    return false;
}

function x(e) {
    var t = "boundary=",
        r = "",
        n = e.search(t);
    return -1 != n && (n += t.length, '"' == (r = -1 == (t = e.search("\r\n")) ? 
           e.substr(n) : e.substr(n, t)).charAt(0) && 
           (r = r.slice(1, r.length - 1))), r
}

The plugin communicates with a local server running on port 55296. This server is a part of the broader Forcepoint Websense DLP system.

function I() {
    return l ? "https://localhost:55296/ChromeExt/" : "http://localhost:55296/ChromeExt/"
}

When the extension intercepts a request, it forwards relevant information to this local server. The extension includes detailed metadata about each request in custom headers, allowing the local server to make informed decisions about whether to allow or block the request.

var y = new XMLHttpRequest;
y.open("POST", I() + X, b);
y.setRequestHeader("X-Email-Url", r.url);
y.setRequestHeader("X-Status", r.statusCode);
y.setRequestHeader("X-Referrer", r.originUrl);
y.setRequestHeader("X-Initiator", r.initiator);
y.setRequestHeader("X-Private", "false");

The extension pays special attention to email systems, particularly Gmail and Yahoo Mail. It includes specific patterns for monitoring these services. When an error occurs during email attachment uploads, the extension can trigger a page reload to ensure proper functionality.

var H = "ALLOW",
    O = !1,
    U = !1,
    D = {},
    X = "12345678",
    o = {},
    e = ["https://mail.google.com/*", "https://mail.yahoo.com/*"],
    r = ["mail\\.google\\.com\\/sync", "mail\\.yahoo\\.com\\/ws\\/v3\\/batch\\?name=messages"],
    a = {},
    l = !0;

// [...]

chrome.webRequest.onErrorOccurred.addListener(function(t) {
    if (void 0 !== t && "POST" == t.method && 
        "net::ERR_BLOCKED_BY_CLIENT" == t.error) {
        let e = new RegExp(r.join("|"), "i");
        e.test(t.url) && chrome.tabs.query({
            active: true,
            currentWindow: true
        }, function(e) {
            chrome.tabs.reload(e[0].id)
        })
    }
}, {
    urls: e
});

The content script component of the extension monitors file input elements on web pages, allowing it to detect when users attempt to upload files. This monitoring system uses MutationObserver to detect dynamically added file inputs, ensuring comprehensive coverage of all possible file upload attempts on a page.

function t() {
    var e;
    for (e of document.body.getElementsByTagName("input")) 
        if ("file" === e.type && void 0 === e._fp_processed) {
            e.addEventListener("click", o, true);
            e._fp_processed = true;
        }
}

const e = new MutationObserver(function(e) {
    t()
});
e.observe(document.body, {
    childList: true,
    subtree: true
});

Verifications

At the core of the extension's security is its authentication system with a local server. This system ensures that the extension is genuine and actively managed by the organization's security infrastructure. The authentication process begins with a periodic check to the local server. The plugin establishes a heartbeat connection with the local security server. The extension validates itself every 15 seconds, switching between HTTPS and HTTP if needed, and maintains a state flag (O) that determines whether the extension is authorized to intercept traffic. The use of the extension ID in the authentication request creates a unique identifier for each installation.

function i() {
    var t, e = new XMLHttpRequest;
    e.onreadystatechange = function() {
        4 == e.readyState && (200 == e.status ? 
            (U = "MOMOMO" == e.responseText, O = !0) : 
            0 == e.status || 500 == e.status ? O = !1 : 
            (O = !0, console.log("GET response ERROR code: " + e.status)))
    };
    e.addEventListener("error", function(e) {
        l ? (l = !1, i(), clearTimeout(t)) : l = !0
    });
    e.open("GET", I() + chrome.runtime.id, !0);
    e.send();
    t = setTimeout(i, 15e3)
}

The extension implements a request tracking system that prevents duplicate uploads and maintains an audit trail. This system uses MD5 hashing to create unique fingerprints of requests.

var v = L(m += l + w);
T();
if (v in D) {
    return { cancel: true };
}
D[v] = r.timeStamp;

Content Parsing

The extension performs content inspection, particularly focusing on file uploads and attachments. It implements specialized parsers for different types of content, including multipart form data and specialized upload protocols.

function x(e) {
    var t = "boundary=",
        r = "",
        n = e.search(t);
    if (-1 != n) {
        n += t.length;
        r = -1 == (t = e.search("\r\n")) ? 
            e.substr(n) : e.substr(n, t);
        if ('"' == r.charAt(0)) {
            r = r.slice(1, r.length - 1);
        }
    }
    return r;
}

This boundary detection function is crucial for accurately parsing multipart form data, which is commonly used for file uploads. The extension uses this information to properly separate and analyze individual parts of the upload content.

The extension implements a comprehensive file upload control system that operates at multiple levels. At the DOM level, it monitors file input elements.

function t() {
    var e;
    for (e of document.body.getElementsByTagName("input")) {
        if ("file" === e.type && void 0 === e._fp_processed) {
            e.addEventListener("click", o, !0);
            e._fp_processed = !0;
        }
    }
}

const e = new MutationObserver(function(e) {
    t()
});
e.observe(document.body, {
    childList: !0,
    subtree: !0
});

This creates a monitoring system that catches all file upload attempts, even on dynamically loaded content. When a file input is clicked, the extension notifies the background script.

function o(e) {
    window.addEventListener("focus", n);
    chrome.runtime.sendMessage({
        type: "open_file_dialog"
    });
}

The extension includes specialized handling for Microsoft OneDrive's upload protocol. This parser specifically looks for OneDrive's upload session creation endpoints. It decodes the URL and extracts the relevant path information. The function is designed to handle OneDrive's specific URL format where file paths are embedded within the URL structure.

function P(e) {
    var t = decodeURIComponent(e),
        r = "",
        n = t.lastIndexOf(":/oneDrive.createUploadSession");
    if (-1 != n) {
        e = t.substr(0, n).lastIndexOf(":/");
        if (-1 != e) {
            r = t.slice(e += ":/".length, n);
        }
    }
    return r;
}

The extension implements specialized parsing for Microsoft 365 SharePoint URLs and uploads. This parser handles SharePoint's specific URL encoding format. It first decodes the URL, then looks for specific patterns that indicate file operations. The parser is designed to extract the actual file path from SharePoint's complex URL structure.

function E(e) {
    var t = decodeURIComponent(e),
        r = y(t);
    if ("" == r) return "";

    e = t.indexOf(r += "='");
    if (-1 == e) return "";

    e += r.length;
    r = t.substr(e).indexOf("'");
    return -1 == r ? t.substr(e) : t.substr(e, r)
}

The extension also includes special handling for Google Drive's upload protocol. Google Drive uses different upload protocols depending on the file size and type, with smaller files using a simple direct upload where the entire file is sent in a single request.

function B(e) {
    for (var t of e)
        if ("x-goog-upload-protocol" == t.name.toLowerCase()) 
            return true;
    return false;
}

When multi-part uploads are detected, the extension applies specific processing rules. Multi-part connections happen when uploading multiple files simultaneously, when metadata needs to be sent along with the file content, when using the browser's FormData API for uploads, or when uploading through the Google Drive API's multipart endpoint.

if ("PUT" == r.method && 0 == B(r.requestHeaders)) {
    S(r.requestId);
    return { cancel: false };
}

The extension implements a form data processing that can handle both standard form data and file uploads. This code shows how the extension processes form data, handling both file names and actual content. It includes size limits (524288 bytes) to prevent memory issues with large uploads, and it carefully formats the multipart data according to HTTP standards.

if (c.formData && null != c.formData) {
    for (var R in c.formData) {
        for (let e = 0; e < c.formData[R].length; e++) {
            if (null != c.formData[R][e]) {
                if (null != c.formData[R][e].match(/^[\w\s-\.\)\(]+\.[\w]{1,5}$/)) {
                    l += c.formData[R][e] + "\n";
                } else {
                    if ("" != i) {
                        s += "\r\n--" + i;
                        s += "\r\nContent-Disposition: form-data; ";
                        s += 'name="' + R + '"\r\n\r\n';
                    }
                    s += c.formData[R][e].substring(0, 
                         Math.min(524288, c.formData[R][e].length));
                }
            }
        }
    }
}

For raw binary uploads, the extension implements specialized buffer handling. It uses TypedArrays and ArrayBuffers for binary data handling, crucial for processing large file uploads by combining multiple chunks into a single buffer when necessary.

if (null != c.raw) {
    for (let e = 0; e < c.raw.length; e++) {
        if (null != c.raw[e].bytes) {
            u = null == u ? c.raw[e].bytes : (
                t = new ArrayBuffer(u.byteLength + c.raw[e].bytes.byteLength),
                n = new Uint8Array(t),
                a = new Uint8Array(u),
                o = new Uint8Array(c.raw[e].bytes),
                n.set(a, 0),
                n.set(o, a.length),
                t
            );
        } else if (null != c.raw[e].file) {
            l += c.raw[e].file + "\n";
        }
    }
}

Policy Enforcement

The extension communicates with the local server to make blocking decisions. It sends detailed metadata about each request, including the URL, status, referrer, and file information. The server's response determines whether the request should be allowed or blocked.

var y = new XMLHttpRequest;
y.open("POST", I() + X, b);
y.setRequestHeader("X-Email-Url", e.url);
y.setRequestHeader("X-Status", e.statusCode);
y.setRequestHeader("X-Referrer", e.originUrl);
y.setRequestHeader("X-Initiator", e.initiator);
y.setRequestHeader("X-Private", "false");
y.setRequestHeader("X-Attach-File", btoa(unescape(encodeURIComponent(m))));

try {
    y.send(w);
} catch (e) {
    O = !1;
}

if (4 == y.readyState) {
    if (200 == y.status) {
        H = y.responseText;
    } else if (0 == y.status || 500 == y.status) {
        O = !1;
    } else {
        console.log("POST response ERROR code: " + y.status);
        H = "BLOCK";
    }
}

Conclusions

To be fully honest, I’m not really sure about whats the solution here. Alternative approaches like proxy-based monitoring, HTTPS inspection, network drivers, or OS-level hooks fall short because they either cannot handle encrypted traffic without complex PKI infrastructure, lack browser-specific context, or miss operations happening within the browser sandbox.

But what i can say is that DLP solutions are often employed by overly paranoid workplaces seeking to monitor their user behaviors. And instead of pitching their capabilities to protect data from leaking or being exposed publically, many DLP vendors tout more… creative ways to use their product.

Limiting what people can read on Twitter? Blocking likes in Facebook? What business or security case you can make for doing such things? Its very clear who these tools are marketed towards, and they’re not even hiding it. I also take issue with the fact that they’re pitching these for firms who wanna install them on the personal devices of workers, which is a whole other can of worms.

Honestly, the solution is just don’t use these things. Trust me, there is no business case to spy on my three hour doomscrolling session.