Drift is easy to drop into a page, which is exactly why people get sloppy with it.
I’ve seen teams treat chat widgets like harmless marketing glue: paste the vendor snippet, pass a few user fields, and move on. That mindset creates XSS problems fast. The widget itself may be hosted by Drift, but the dangerous part is usually the code around it: how you inject the snippet, how you pass user-controlled data, and how your app renders data collected through chat flows.
If you use Drift on a production app, you need to think about XSS in three places:
- Bootstrapping the widget
- Sending user data into Drift
- Rendering Drift-related data back into your own DOM
Where XSS actually happens
Most XSS bugs involving Drift are not “Drift is vulnerable.” They are usually one of these:
- Injecting the Drift script dynamically with unsafe string concatenation
- Writing user data into inline
<script>blocks - Passing untrusted profile fields that later get rendered unsafely in your UI
- Displaying chat transcripts, contact names, or custom attributes with
innerHTML - Allowing overly broad CSP rules because “the widget needs it”
Here’s a classic bad pattern:
<script>
var username = "{{ user.display_name }}";
drift.identify("123", {
name: username
});
</script>
If user.display_name is not correctly encoded for a JavaScript string context, an attacker can break out:
";alert(1);//
Now your page executes attacker-controlled JavaScript before Drift even finishes loading.
That’s not a Drift bug. That’s your template outputting untrusted data into an inline script.
Unsafe Drift installation pattern
A lot of teams wrap third-party widgets in convenience helpers. That helper often becomes the vulnerability.
Bad example:
function loadDrift(widgetId) {
document.body.innerHTML += `
<script>
!function() {
var t = window.driftt = window.drift = window.driftt || [];
if (!t.init) {
t.invoked = !0;
t.methods = ["identify", "config", "track"];
t.factory = function(e) {
return function() {
var n = Array.prototype.slice.call(arguments);
n.unshift(e);
t.push(n);
return t;
};
};
t.methods.forEach(function(e) {
t[e] = t.factory(e);
});
t.load = function(id) {
var e = 3e5;
var n = Math.ceil(new Date() / e) * e;
var o = document.createElement("script");
o.type = "text/javascript";
o.async = true;
o.crossorigin = "anonymous";
o.src = "https://js.driftt.com/include/" + n + "/" + id + ".js";
var i = document.getElementsByTagName("script")[0];
i.parentNode.insertBefore(o, i);
};
}
}();
drift.load("${widgetId}");
</script>
`;
}
This is bad for multiple reasons:
innerHTMLis used to inject a script blockwidgetIdis interpolated into executable code- CSP becomes harder because this relies on inline script
- any attacker-controlled value in
widgetIdbecomes dangerous
Even if widgetId is “internal,” I don’t trust future refactors.
Safer widget bootstrapping
Load Drift using DOM APIs, not HTML string injection.
function initDrift(widgetId) {
if (!/^[a-zA-Z0-9]+$/.test(widgetId)) {
throw new Error("Invalid Drift widget ID");
}
const drift = (window.driftt = window.drift = window.driftt || []);
if (drift.init) return;
drift.invoked = true;
drift.methods = ["identify", "config", "track", "reset", "debug", "show", "ping", "page", "hide", "off", "on"];
drift.factory = function(method) {
return function() {
const args = Array.prototype.slice.call(arguments);
args.unshift(method);
drift.push(args);
return drift;
};
};
drift.methods.forEach((method) => {
drift[method] = drift.factory(method);
});
drift.load = function(id) {
const interval = 300000;
const timestamp = Math.ceil(Date.now() / interval) * interval;
const script = document.createElement("script");
script.async = true;
script.src = `https://js.driftt.com/include/${timestamp}/${id}.js`;
script.crossOrigin = "anonymous";
document.head.appendChild(script);
};
drift.load(widgetId);
}
This is still third-party script loading, but at least you’re not mixing untrusted data with HTML parsing or inline execution.
Don’t inject user data into JavaScript
The most common XSS bug around Drift identity is server-side templating inside inline JS.
Bad:
<script>
drift.identify("{{ user.id }}", {
email: "{{ user.email }}",
name: "{{ user.name }}",
plan: "{{ user.plan }}"
});
</script>
If your template engine doesn’t apply JavaScript-string-safe escaping, this is fragile.
A safer pattern is to put data in HTML as inert JSON, then parse it.
<script type="application/json" id="drift-user-data">
{
"id": "12345",
"email": "[email protected]",
"name": "Alice",
"plan": "pro"
}
</script>
Then:
function getDriftUserData() {
const el = document.getElementById("drift-user-data");
if (!el) return null;
try {
return JSON.parse(el.textContent);
} catch {
return null;
}
}
const user = getDriftUserData();
if (user) {
drift.identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan
});
}
That avoids the “escape for JavaScript string context” problem. You still need server-side JSON encoding, but that’s generally easier to get right than hand-building JS.
Treat Drift profile fields as untrusted
A weird mistake I keep seeing: developers assume fields sent to Drift are “safe” because they originated from their own app.
Nope.
If a field can ever contain attacker input, it is untrusted forever.
For example, if you send this:
drift.identify(user.id, {
name: user.name,
company: user.company,
bio: user.bio
});
and later show those same values in your admin dashboard:
profilePanel.innerHTML = `
<h3>${contact.name}</h3>
<p>${contact.company}</p>
<div>${contact.bio}</div>
`;
you’ve created stored XSS in your own app. The payload may have traveled through Drift, but your DOM sink is the actual exploit point.
Use text-only rendering unless you absolutely need HTML.
Safe version:
function renderContact(contact) {
const nameEl = document.getElementById("contact-name");
const companyEl = document.getElementById("contact-company");
const bioEl = document.getElementById("contact-bio");
nameEl.textContent = contact.name || "";
companyEl.textContent = contact.company || "";
bioEl.textContent = contact.bio || "";
}
If you really must allow limited formatting, sanitize it with a trusted HTML sanitizer before assigning to innerHTML. My strong preference is to avoid HTML entirely for chat/profile metadata.
Watch custom event handlers
Drift integrations often hook into app logic:
drift.on("message", function(event) {
showNotification(event.data.author.name);
});
That’s fine until someone “improves” the notification UI like this:
function showNotification(name) {
const el = document.getElementById("toast");
el.innerHTML = `<strong>${name}</strong> sent you a message`;
}
Now author.name becomes an XSS vector if attacker-controlled data ever reaches it.
Fix it by using DOM node creation:
function showNotification(name) {
const el = document.getElementById("toast");
el.textContent = "";
const strong = document.createElement("strong");
strong.textContent = name;
el.appendChild(strong);
el.appendChild(document.createTextNode(" sent you a message"));
}
This is boring code. Boring code is good security code.
CSP for Drift without giving up completely
A lot of teams respond to third-party widgets by gutting CSP:
Content-Security-Policy: script-src * 'unsafe-inline' 'unsafe-eval'; connect-src *; frame-src *;
That’s basically no CSP.
You want a CSP that allows Drift, but still blocks inline script where possible and keeps the rest of the app locked down. Exact directives depend on your integration, but the shape should look more like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://js.driftt.com 'nonce-r4nd0m';
connect-src 'self' https://*.drift.com https://*.driftt.com;
frame-src https://*.drift.com https://*.driftt.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
frame-ancestors 'self';
report-to default-endpoint;
If you still use Drift’s inline bootstrap snippet, use a nonce. Better yet, move bootstrap code into your own external script so you can avoid 'unsafe-inline'.
For implementation details around CSP rollout and strict policies, https://csp-guide.com is useful. You should also verify the current Drift domains against Drift’s official documentation because vendors do change infrastructure over time.
A hardened example
Here’s a more realistic pattern I’d actually ship.
Server-rendered HTML:
<script type="application/json" id="drift-config">
{
"widgetId": "abc123xyz",
"user": {
"id": "12345",
"email": "[email protected]",
"name": "Alice Example",
"plan": "pro"
}
}
</script>
<script src="/static/js/drift-init.js" defer></script>
Client code:
function readJsonScript(id) {
const el = document.getElementById(id);
if (!el) return null;
try {
return JSON.parse(el.textContent);
} catch {
return null;
}
}
function validWidgetId(value) {
return typeof value === "string" && /^[a-zA-Z0-9]+$/.test(value);
}
function loadDrift(widgetId) {
const drift = (window.driftt = window.drift = window.driftt || []);
if (drift.init) return;
drift.invoked = true;
drift.methods = ["identify", "config", "track", "on"];
drift.factory = (method) => (...args) => {
drift.push([method, ...args]);
return drift;
};
for (const method of drift.methods) {
drift[method] = drift.factory(method);
}
const interval = 300000;
const timestamp = Math.ceil(Date.now() / interval) * interval;
const script = document.createElement("script");
script.async = true;
script.src = `https://js.driftt.com/include/${timestamp}/${widgetId}.js`;
script.crossOrigin = "anonymous";
document.head.appendChild(script);
}
const config = readJsonScript("drift-config");
if (config && validWidgetId(config.widgetId)) {
loadDrift(config.widgetId);
if (config.user && typeof config.user.id === "string") {
drift.identify(config.user.id, {
email: String(config.user.email || ""),
name: String(config.user.name || ""),
plan: String(config.user.plan || "")
});
}
}
This isn’t magic. It just removes the common XSS footguns:
- no
innerHTMLfor script injection - no inline JS data interpolation
- widget ID is validated
- user fields are treated as plain strings
- CSP can stay reasonably strict
What to audit in an existing Drift integration
If I’m reviewing a codebase, I check these first:
- Any use of
innerHTML,outerHTML, orinsertAdjacentHTMLnear Drift data - Inline
<script>blocks containing templated user data - Dynamic script creation using unvalidated values
- Admin/support dashboards rendering contact attributes or transcripts
- CSP exceptions added “for Drift”
- Custom notification, transcript, or CRM panels fed by Drift events
If you find a bug, don’t just patch the sink. Trace the entire flow:
- where the attacker controls input
- where the data is stored or forwarded
- where it is rendered
- what encoding or sanitization is missing
That’s how XSS around chat widgets survives for years: the widget gets blamed, while the real issue is your application trusting data it should never trust.
Drift can be integrated safely. You just need to treat it like any other boundary where untrusted data crosses into your frontend. That means safe DOM APIs, context-aware encoding, strict CSP, and zero tolerance for convenience innerHTML.