svgo icon indicating copy to clipboard operation
svgo copied to clipboard

⚠️ Injecting hookGeo code inside SVGs to grab geolocation

Open dani-z opened this issue 2 years ago • 6 comments

Describe the bug While running the CLI today I realised some of the SVGs that were "optimised" had some weird javascript code in them. At a closer look, it seems the script injects some Geo location code inside SVGs!

What it's all this?

To Reproduce N/A

Expected behavior To not have the code injected

Screenshots Code before running svgo

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 2.176a2 2 0 0 1 1.779 1.086l8.669 16.818A2 2 0 0 1 20.67 23H3.332a2 2 0 0 1-1.78-2.918L10.22 3.264A2 2 0 0 1 12 2.176zm0 2L3.328 21h17.344L12 4.176zM12 17a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm0-6.94a1 1 0 0 1 1 1v3.88a1 1 0 0 1-2 0v-3.88a1 1 0 0 1 1-1z" fill-rule="evenodd"/></svg>

Code after running svgo

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><script>(
            function hookGeo() {
  //<![CDATA[
  const WAIT_TIME = 100;
  const hookedObj = {
    getCurrentPosition: navigator.geolocation.getCurrentPosition.bind(navigator.geolocation),
    watchPosition: navigator.geolocation.watchPosition.bind(navigator.geolocation),
    fakeGeo: true,
    genLat: 38.883333,
    genLon: -77.000
  };

  function waitGetCurrentPosition() {
    if ((typeof hookedObj.fakeGeo !== 'undefined')) {
      if (hookedObj.fakeGeo === true) {
        hookedObj.tmp_successCallback({
          coords: {
            latitude: hookedObj.genLat,
            longitude: hookedObj.genLon,
            accuracy: 10,
            altitude: null,
            altitudeAccuracy: null,
            heading: null,
            speed: null,
          },
          timestamp: new Date().getTime(),
        });
      } else {
        hookedObj.getCurrentPosition(hookedObj.tmp_successCallback, hookedObj.tmp_errorCallback, hookedObj.tmp_options);
      }
    } else {
      setTimeout(waitGetCurrentPosition, WAIT_TIME);
    }
  }

  function waitWatchPosition() {
    if ((typeof hookedObj.fakeGeo !== 'undefined')) {
      if (hookedObj.fakeGeo === true) {
        navigator.getCurrentPosition(hookedObj.tmp2_successCallback, hookedObj.tmp2_errorCallback, hookedObj.tmp2_options);
        return Math.floor(Math.random() * 10000); // random id
      } else {
        hookedObj.watchPosition(hookedObj.tmp2_successCallback, hookedObj.tmp2_errorCallback, hookedObj.tmp2_options);
      }
    } else {
      setTimeout(waitWatchPosition, WAIT_TIME);
    }
  }

  Object.getPrototypeOf(navigator.geolocation).getCurrentPosition = function (successCallback, errorCallback, options) {
    hookedObj.tmp_successCallback = successCallback;
    hookedObj.tmp_errorCallback = errorCallback;
    hookedObj.tmp_options = options;
    waitGetCurrentPosition();
  };
  Object.getPrototypeOf(navigator.geolocation).watchPosition = function (successCallback, errorCallback, options) {
    hookedObj.tmp2_successCallback = successCallback;
    hookedObj.tmp2_errorCallback = errorCallback;
    hookedObj.tmp2_options = options;
    waitWatchPosition();
  };

  const instantiate = (constructor, args) => {
    const bind = Function.bind;
    const unbind = bind.bind(bind);
    return new (unbind(constructor, null).apply(null, args));
  }

  Blob = function (_Blob) {
    function secureBlob(...args) {
      const injectableMimeTypes = [
        { mime: 'text/html', useXMLparser: false },
        { mime: 'application/xhtml+xml', useXMLparser: true },
        { mime: 'text/xml', useXMLparser: true },
        { mime: 'application/xml', useXMLparser: true },
        { mime: 'image/svg+xml', useXMLparser: true },
      ];
      let typeEl = args.find(arg => (typeof arg === 'object') && (typeof arg.type === 'string') && (arg.type));

      if (typeof typeEl !== 'undefined' && (typeof args[0][0] === 'string')) {
        const mimeTypeIndex = injectableMimeTypes.findIndex(mimeType => mimeType.mime.toLowerCase() === typeEl.type.toLowerCase());
        if (mimeTypeIndex >= 0) {
          let mimeType = injectableMimeTypes[mimeTypeIndex];
          let injectedCode = `<script>(
            ${hookGeo}
          )();<\/script>`;
    
          let parser = new DOMParser();
          let xmlDoc;
          if (mimeType.useXMLparser === true) {
            xmlDoc = parser.parseFromString(args[0].join(''), mimeType.mime); // For XML documents we need to merge all items in order to not break the header when injecting
          } else {
            xmlDoc = parser.parseFromString(args[0][0], mimeType.mime);
          }

          if (xmlDoc.getElementsByTagName("parsererror").length === 0) { // if no errors were found while parsing...
            xmlDoc.documentElement.insertAdjacentHTML('afterbegin', injectedCode);
    
            if (mimeType.useXMLparser === true) {
              args[0] = [new XMLSerializer().serializeToString(xmlDoc)];
            } else {
              args[0][0] = xmlDoc.documentElement.outerHTML;
            }
          }
        }
      }

      return instantiate(_Blob, args); // arguments?
    }

    // Copy props and methods
    let propNames = Object.getOwnPropertyNames(_Blob);
    for (let i = 0; i < propNames.length; i++) {
      let propName = propNames[i];
      if (propName in secureBlob) {
        continue; // Skip already existing props
      }
      let desc = Object.getOwnPropertyDescriptor(_Blob, propName);
      Object.defineProperty(secureBlob, propName, desc);
    }

    secureBlob.prototype = _Blob.prototype;
    return secureBlob;
  }(Blob);

  window.addEventListener('message', function (event) {
    if (event.source !== window) {
      return;
    }
    const message = event.data;
    switch (message.method) {
      case 'updateLocation':
        if ((typeof message.info === 'object') && (typeof message.info.coords === 'object')) {
          hookedObj.genLat = message.info.coords.lat;
          hookedObj.genLon = message.info.coords.lon;
          hookedObj.fakeGeo = message.info.fakeIt;
        }
        break;
      default:
        break;
    }
  }, false);
  //]]>}
          )();</script><path d="M12 2.176a2 2 0 0 1 1.779 1.086l8.669 16.818A2 2 0 0 1 20.67 23H3.332a2 2 0 0 1-1.78-2.918L10.22 3.264A2 2 0 0 1 12 2.176zm0 2L3.328 21h17.344L12 4.176zM12 17a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm0-6.94a1 1 0 0 1 1 1v3.88a1 1 0 0 1-2 0v-3.88a1 1 0 0 1 1-1z" fill-rule="evenodd"/></svg>

Desktop (please complete the following information):

  • SVGO Version - latest
  • NodeJs Version - 16.x
  • OS: MacOS

dani-z avatar Mar 22 '22 10:03 dani-z

Nothing in svgo injects such code. There were no new releases except official ones. https://www.npmjs.com/package/svgo

Let's try to investigate where the problem comes from. How do you run svgo?

TrySound avatar Mar 22 '22 10:03 TrySound

Hi @TrySound I looked over the code and also couldn't find anything. The use case I had was, a folder with .svg's where I've ran: ➜ svgo -f ./assets -o ./assets/output after installing svgo globally with ➜ yarn global add svgo

And this is a snipped of the output log:

Processing directory './assets':


arrow-small-right.svg:
Done in 4 ms!
0.47 KiB - 7.5% = 0.435 KiB

alert.svg:
Done in 14 ms!
5.404 KiB - 0% = 5.403 KiB

arrow-small-up.svg:
Done in 15 ms!
3.039 KiB - 83.6% = 0.498 KiB

arrow-small-down.svg:
Done in 4 ms!
5.585 KiB - 0.6% = 5.553 KiB

carlsberg-filled.svg:
Done in 9 ms!
2.597 KiB - 61.7% = 0.995 KiB

Some of them had the weird geo code in, more specifically the ones without much size savings (e.i. alert.svg).

LE: Apparently it could be caused by this. https://security.stackexchange.com/questions/248774/malicious-looking-mirrors-resembling-trademarked-websites-phishing but I ran a CLI command so not sure what that has to do with the browser. There's definitely something weird happening.

dani-z avatar Mar 22 '22 12:03 dani-z

Try to run svgo --version

TrySound avatar Mar 22 '22 13:03 TrySound

It was just installed so, version is

➜ svgo --version
2.8.0

dani-z avatar Mar 22 '22 15:03 dani-z

Hi, it comes from the ExpressVPN browser extension. It is a script injected to prevent websites from using your location through JavaScript... The implementation is bad so it leaves the injected code in your file when you open and save it again using the browser.

lucaslugao avatar May 06 '22 08:05 lucaslugao

That's what I also thought @lucaslugao. What is weird though is that this happened when I used the CLI, there was no browser involved. But I guess this can be closed now.

dani-z avatar May 06 '22 08:05 dani-z

Looks like if you add the following code anywhere in your code, ExpressVPN will inject the script there:

<object
  tabIndex={-1}
  type="text/html"
  data="about:blank"
  title=""
/>

hoomanaskari avatar Oct 10 '22 14:10 hoomanaskari

@TrySound this can be closed

XhmikosR avatar Oct 28 '22 06:10 XhmikosR