svgo
svgo copied to clipboard
⚠️ Injecting hookGeo code inside SVGs to grab geolocation
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
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?
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.
Try to run svgo --version
It was just installed so, version is
➜ svgo --version
2.8.0
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.
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.
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=""
/>
@TrySound this can be closed