web-eid / web-eid-webextension

The Web eID browser extension for Chrome, Edge, Firefox and other WebExtensions-compatible browsers
https://web-eid.eu
MIT License
15 stars 18 forks source link

Web eID extension is injecting JS code into emails in Mautic software #46

Closed matbcvo closed 1 year ago

matbcvo commented 1 year ago

Web eID extension is injecting JS code into our emails in Mautic software (while editing email content/template). This affects our email deliverability as all outgoing emails from Mautic are ending up in receiver's spam folder because of injected JS code from this extension. We cannot even disable this extension in the Google Chrome browser, solution would be to uninstall eID software which we do not want, because we need to sign documents too.

// edit: Currently we made plugin for Mautic that strips this JS code from email before being sent out, so this issue is fixed for us. I still think that this extension shouldn't inject JS everywhere.

From email body:

--_=_swift_1668510684_7119536567990e726f3ae2452431232c_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<!DOCTYPE html><!DOCTYPE html><html lang=3D"en" xmlns=3D"http://www.w3.org/=
1999/xhtml" xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schema=
s-microsoft-com:office:office" class=3D" responsejs " style=3D""><head><scr=
ipt type=3D"text/javascript">// Promises=20
var _eid_promises =3D {};=20
// Turn the incoming message from extension=20
// into pending Promise resolving=20
window.addEventListener("message", function(event) {=20
    if(event.source !=3D=3D window) return;=20
    if(event.data.src &amp;amp;&amp;amp; (event.data.src =3D=3D=3D "backgro=
und.js")) {=20
        console.log("Page received: ");=20
        console.log(event.data);=20
        // Get the promise=20
        if(event.data.nonce) {=20
            var p =3D _eid_promises[event.data.nonce];=20
            // resolve=20
            if(event.data.result =3D=3D=3D "ok") {=20
                if(event.data.signature !=3D=3D undefined) {=20
                    p.resolve({hex: event.data.signature});=20
                } else if(event.data.version !=3D=3D undefined) {=20
                    p.resolve(event.data.extension + "/" + event.data.versi=
on);=20
                } else if(event.data.cert !=3D=3D undefined) {=20
                    p.resolve({hex: event.data.cert});=20
                } else {=20
                    console.log("No idea how to handle message");=20
                    console.log(event.data);=20
                }=20
            } else {=20
                // reject=20
                p.reject(new Error(event.data.result));=20
            }=20
            delete _eid_promises[event.data.nonce];=20
        } else {=20
            console.log("No nonce in event msg");=20
        }=20
    }=20
}, false);=20
=20
=20
function TokenSigning() {=20
    function nonce() {=20
        var val =3D "";=20
        var hex =3D "abcdefghijklmnopqrstuvwxyz0123456789";=20
        for(var i =3D 0; i &amp;lt; 16; i++) val +=3D hex.charAt(Math.floor=
(Math.random() * hex.length));=20
        return val;=20
    }=20
=20
    function messagePromise(msg) {=20
        return new Promise(function(resolve, reject) {=20
            // amend with necessary metadata=20
            msg["nonce"] =3D nonce();=20
            msg["src"] =3D "page.js";=20
            // send message=20
            window.postMessage(msg, "*");=20
            // and store promise callbacks=20
            _eid_promises[msg.nonce] =3D {=20
                resolve: resolve,=20
                reject: reject=20
            };=20
        });=20
    }=20
    this.getCertificate =3D function(options) {=20
        var msg =3D {type: "CERT", lang: options.lang, filter: options.filt=
er};=20
        console.log("getCertificate()");=20
        return messagePromise(msg);=20
    };=20
    this.sign =3D function(cert, hash, options) {=20
        var msg =3D {type: "SIGN", cert: cert.hex, hash: hash.hex, hashtype=
: hash.type, lang: options.lang, info: options.info};=20
        console.log("sign()");=20
        return messagePromise(msg);=20
    };=20
    this.getVersion =3D function() {=20
        console.log("getVersion()");=20
        return messagePromise({=20
            type: "VERSION"=20
        });=20
    };=20
}</script><script type=3D"text/javascript">// Promises=20
var _eid_promises =3D {};=20
// Turn the incoming message from extension=20
// into pending Promise resolving=20
window.addEventListener("message", function(event) {=20
    if(event.source !=3D=3D window) return;=20
    if(event.data.src &amp;amp;amp;amp;&amp;amp;amp;amp; (event.data.src =
=3D=3D=3D "background.js")) {=20
        console.log("Page received: ");=20
        console.log(event.data);=20
        // Get the promise=20
        if(event.data.nonce) {=20
            var p =3D _eid_promises[event.data.nonce];=20
            // resolve=20
            if(event.data.result =3D=3D=3D "ok") {=20
                if(event.data.signature !=3D=3D undefined) {=20
                    p.resolve({hex: event.data.signature});=20
                } else if(event.data.version !=3D=3D undefined) {=20
                    p.resolve(event.data.extension + "/" + event.data.versi=
on);=20
                } else if(event.data.cert !=3D=3D undefined) {=20
                    p.resolve({hex: event.data.cert});=20
                } else {=20
                    console.log("No idea how to handle message");=20
                    console.log(event.data);=20
                }=20
            } else {=20
                // reject=20
                p.reject(new Error(event.data.result));=20
            }=20
            delete _eid_promises[event.data.nonce];=20
        } else {=20
            console.log("No nonce in event msg");=20
        }=20
    }=20
}, false);=20
function TokenSigning() {=20
    function nonce() {=20
        var val =3D "";=20
        var hex =3D "abcdefghijklmnopqrstuvwxyz0123456789";=20
        for(var i =3D 0; i &amp;amp;amp;lt; 16; i++) val +=3D hex.charAt(Ma=
th.floor(Math.random() * hex.length));=20
        return val;=20
    }=20
    function messagePromise(msg) {=20
        return new Promise(function(resolve, reject) {=20
            // amend with necessary metadata=20
            msg["nonce"] =3D nonce();=20
            msg["src"] =3D "page.js";=20
            // send message=20
            window.postMessage(msg, "*");=20
            // and store promise callbacks=20
            _eid_promises[msg.nonce] =3D {=20
                resolve: resolve,=20
                reject: reject=20
            };=20
        });=20
    }=20
    this.getCertificate =3D function(options) {=20
        var msg =3D {type: "CERT", lang: options.lang, filter: options.filt=
er};=20
        console.log("getCertificate()");=20
        return messagePromise(msg);=20
    };=20
    this.sign =3D function(cert, hash, options) {=20
        var msg =3D {type: "SIGN", cert: cert.hex, hash: hash.hex, hashtype=
: hash.type, lang: options.lang, info: options.info};=20
        console.log("sign()");=20
        return messagePromise(msg);=20
    };=20
    this.getVersion =3D function() {=20
        console.log("getVersion()");=20
        return messagePromise({=20
            type: "VERSION"=20
        });=20
    };=20
}</script><script type=3D"text/javascript">// Promises=20
var _eid_promises =3D {};=20
// Turn the incoming message from extension=20
// into pending Promise resolving=20
window.addEventListener("message", function(event) {=20
    if(event.source !=3D=3D window) return;=20
    if(event.data.src &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; (event.dat=
a.src =3D=3D=3D "background.js")) {=20
        console.log("Page received: ");=20
        console.log(event.data);=20
        // Get the promise=20
        if(event.data.nonce) {=20
            var p =3D _eid_promises[event.data.nonce];=20
            // resolve=20
            if(event.data.result =3D=3D=3D "ok") {=20
                if(event.data.signature !=3D=3D undefined) {=20
                    p.resolve({hex: event.data.signature});=20
                } else if(event.data.version !=3D=3D undefined) {=20
                    p.resolve(event.data.extension + "/" + event.data.versi=
on);=20
                } else if(event.data.cert !=3D=3D undefined) {=20
                    p.resolve({hex: event.data.cert});=20
                } else {=20
                    console.log("No idea how to handle message");=20
                    console.log(event.data);=20
                }=20
            } else {=20
                // reject=20
                p.reject(new Error(event.data.result));=20
            }=20
            delete _eid_promises[event.data.nonce];=20
        } else {=20
            console.log("No nonce in event msg");=20
        }=20
    }=20
}, false);=20
function TokenSigning() {=20
    function nonce() {=20
        var val =3D "";=20
        var hex =3D "abcdefghijklmnopqrstuvwxyz0123456789";=20
        for(var i =3D 0; i &amp;amp;amp;amp;lt; 16; i++) val +=3D hex.charA=
t(Math.floor(Math.random() * hex.length));=20
        return val;=20
    }=20
    function messagePromise(msg) {=20
        return new Promise(function(resolve, reject) {=20
            // amend with necessary metadata=20
            msg["nonce"] =3D nonce();=20
            msg["src"] =3D "page.js";=20
            // send message=20
            window.postMessage(msg, "*");=20
            // and store promise callbacks=20
            _eid_promises[msg.nonce] =3D {=20
                resolve: resolve,=20
                reject: reject=20
            };=20
        });=20
    }=20
    this.getCertificate =3D function(options) {=20
        var msg =3D {type: "CERT", lang: options.lang, filter: options.filt=
er};=20
        console.log("getCertificate()");=20
        return messagePromise(msg);=20
    };=20
    this.sign =3D function(cert, hash, options) {=20
        var msg =3D {type: "SIGN", cert: cert.hex, hash: hash.hex, hashtype=
: hash.type, lang: options.lang, info: options.info};=20
        console.log("sign()");=20
        return messagePromise(msg);=20
    };=20
    this.getVersion =3D function() {=20
        console.log("getVersion()");=20
        return messagePromise({=20
            type: "VERSION"=20
        });=20
    };=20
}</script>
    <title>
        [TEST] DE / BENELUX - AT / IT roundtrips, price: 1.32=E2=82=AC/km (=
empty and loaded)
    </title>
    <meta http-equiv=3D"X-UA-Compatible" content=3D"IE=3Dedge" />
    <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DUTF-8=
" />
    <meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1" />
    <link href=3D"https://fonts.googleapis.com/css?family=3DOpen+Sans:300,4=
00,500,700" rel=3D"stylesheet" type=3D"text/css" />
    <style type=3D"text/css">
        #outlook a {
            padding: 0;
        }
mrts commented 1 year ago

Web eID extension is injecting JS code into our emails in Mautic software (while editing email content/template).

I assume the same problem existed with the TokenSigning extension (in case you used it before)? The hwcrypto.js JavaScript digital signing library relies on this injection technique to communicate with the extension. The library is in wide use and we unfortunately have to maintain compatibility with it for a couple of years, so we cannot disable it in the near future.

We cannot even disable this extension in the Google Chrome browser, solution would be to uninstall eID software which we do not want, because we need to sign documents too.

To activate the extension automatically, the extension is installed with the Chrome enterprise policy. To remove the policy and activate the extension enable/disable toggle button, remove the corresponding value from the SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist Windows registry key. See https://github.com/web-eid/web-eid-app/blob/main/install/web-eid.wxs#L160-L162.


Note that the code is only injected if the page does not contain a script tag with the data-name="TokenSigning" attribute as seen here, so if this attribute is added to any script tag on the corresponding page, the injection does not happen. I understand that you may not want to go this route in this particular case, but I mention it nevertheless just in case.

Another possibility is to use a userscript (with e.g. TamperMonkey, a bookmarklet etc) to remove the injected element.


Finally, I want to thank you for bringing this problem to our attention. We will consider adding an extension configuration page that would allow turning off the injection on certain domains.

mrts commented 1 year ago

I see that you have solved the issue with a Mautic plugin, case closed then!

matbcvo commented 1 year ago

@mrts any news about configuration page that allows to turn off the injection on certain domains? We can't recommend our clients to use userscript to remove that injected element as they do not know how to do that / what is it. We would prefer that than maintaining Mautic plugin. We have 10+ Mautic instances that our clients are using.

taneltm commented 1 year ago

If I've understood the problem, Mautic composes e-mails inside an iframe. I'm guessing, because the iframe content is dynamically updated, it doesn't have an src value.

@mrts Maybe we should consider injecting the backwards compatibility script only when window.location.href is set to an https address? We are checking for secure context, but I think this also applies to an iframe which doesn't have a source and the parent frame has a secure context.

@matbcvo Could you verify if the iframe where Mautic composes the e-mails is missing the src attribute?

matbcvo commented 1 year ago

@taneltm I checked page source and I could not find iframe, seems Mautic does not use that. I'll include page source where you can check it. It even contains multiple "TokenSigning" JS code.

https://gist.githubusercontent.com/matbcvo/2b5c2041fab98cbda592ca78fe522dc5/raw/04193835b18e789f649f054d4d3b5b21ba85a967/mautic-emails-edit.html

taneltm commented 1 year ago

@matbcvo I think Mautic does use iframe, but not always. I don't have much time to investigate this. A proper investigation would probably require me to set up Mautic myself.

But I did go through some of the Mautic code here: https://github.com/mautic/mautic/blob/b56d1fff268e85bee6dcc1043cd274861b7c49c7/app/bundles/CoreBundle/Assets/js/4.builder.js

From the code it's clear that Mautic definitely uses iframes, but because I had limited time, I made quite a lot of assumptions while looking into it.

If it's not too much trouble, could you toggle the "Code mode" when composing an e-mail (I'm assuming that's an option) and see if you can find an iframe then? Should be easy to find an iframe via DevTools: document.getElementsByTagName('iframe')

matbcvo commented 1 year ago

@taneltm You're right. Yes, that's "Builder", where we/our customers compose e-mails content. By checking document.getElementsByTagName('iframe') with DevTools Console, iframe will appear when I click on Builder button. I couldn't find iframe src attribute. Also I ran JS code which confirmed that iframe does not have src attribute.

document.getElementsByTagName('iframe')
HTMLCollection [iframe#builder-template-content, builder-template-content: iframe#builder-template-content]0: iframe#builder-template-content ...

var iframes = document.getElementsByTagName('iframe');

for (var i = 0; i < iframes.length; i++) {
    if (iframes[i].hasAttribute('src')) {
        console.log('iframe ' + i + ' has a src attribute with value:', iframes[i].getAttribute('src'));
    } else {
        console.log('iframe ' + i + ' does not have a src attribute.');
    }
}

VM249:7 iframe 0 does not have a src attribute.
mrts commented 1 year ago

We don't know what the impact is for other websites, so this is risky. I think the safest option for solving this remains the proposed configuration page that allows to turn off the injection on certain domains. However, we don't have time for this in the near future and this will also be just a short-term investment as the injection code will be removed during 2024. @matbcvo, can you perhaps contribute the configuration page code if you feel this is important? The implementation should be not too difficult, see for example this PR that shows a consent page during installation.

taneltm commented 1 year ago

@mrts I doubt there would be an impact for websites which actually need the token signing backwards compatibility in an iFrame.
The sites which need iFrame support will load the iFrame content from a URL. They might have a separate service for authentication which they wish to load inside the iFrame - I do see a point in doing that.
However building an authentication dialog inside an iFrame dynamically with JavaScript from the parent frame seems kinda silly.
That said, the fact remains that we don't know the impact... it's clear that devs do silly things all the time.

@matbcvo Have you tried opening a bug ticket on Mautic's repo? I actually don't think it's a good idea for Mautic to use the iFrame content as a source of truth when composing e-mails.
Web-eID needs to inject code onto a website for for legitimate reasons, but there could be malicious browser extensions which might inject ads, tracking scripts or malware, those would also get injected into e-mails composed by Mautic. That makes it a borderline security issue for Mautic users.
By looking at the code it doesn't seem like a trivial change, but it also doesn't seem too difficult to make Mautic use the iFrame only for displaying the preview and use the textarea/CodeMirror value as the source of truth.