Why are websites requesting access to motion sensors... on my desktop?

Full article

I was checking the status of a FedEx order in Brave, when I noticed a notification in the address bar that I've never seen before. It was warning me that "this site has been blocked from accessing your motion sensors". Wut? It doesn't even need to be an order status - their home page kicks it up too.

I'm struggling to understand why a website would need access to a motion sensor on a mobile device, let alone the fact I was using a desktop. Do I get a different experience if I knock my PC off the desk? Tip my monitor on its side? Grab the mouse cord and spin it around my head really fast?

After a few cursory online searches, I'm coming up with little other than a few threads on Reddit and Brave that indicate people are also seeing this on Kayo Sports and Twitch, as well as Experian and Tutanota.

Guess it's time to dig a little deeper.


What are Web APIs?

Before zeroing in on sensors, let's backup a sec and talk about web design and Web APIs. Your browser has access to a lot of data via (and metadata regarding) the device you installed it on. As much as some of the websites you visit would looove to have access to all that data, any decent browser acts as a firewall, blocking that access by default and prompting you to allow it.

Geolocation API

One of the more common APIs is the one used to request your location, usually when you're using a websites's "store locator" to find the store nearest you.

The button below uses code (lightly modified) from MDN's Geolocation API docs. When you click it, the JavaScript code executes a call to navigator.geolocation.getCurrentPosition(), asking the browser for your location.

   

Your browser prompts you to allow access, which you can deny. Yay privacy.

If you don't see the prompt but you think you've allowed it, there are two different settings that control access - a global page with a list of "blocked" and "allowed" sites, and a per-site page where you can adjust all permissions for a single site. In Chrome, just replace brave:// with chrome:// in the address bar.

Notifications API

Another (unfortunately, very) popular API is the one used to display notifications to visitors. Using the Notifications API, you can request permission from a visitor with a call to Notification.requestPermission() and then just create a new Notification() to annoy them keep them up to date. (May not work in Brave due to a bug.)

Sensors API

There's a (maybe sorta?) new API for requesting access to sensors in Chromium-based browsers (Ghacks puts it at Chrome 75, around June 2019, but wikipedia suggests Chrome 67 around May 2018). It's not widely supported yet. According to MDN, the only major browsers that currently support it are Chrome and Opera, on desktop and mobile.

Check out the MDN docs, the W3C candidate recommendation, the ongoing conversation over at Chrome, and Intel's Sensor API playground for examples.

The following links execute some JavaScript code to try starting up various sensors, which should trigger the sensor icon in the address bar. (If an error occurs, it'll display below the links.)

Status Message: N/A

As with the geolocation and notification APIs, you can grant or deny access at the global or per-site level. What's kind of annoying is that all of the above sensors fall under a single "motion sensors" umbrella, so you can't easily tell which of those sensors a particular site is trying to access.

Why are certain sites requesting the Sensors API?

tl;dr - I don't know yet. Here's what I've dug up so far.

I've seen the sensor request on several sites, and others have reported more - FedEx, Lowes, Kayo Sports, Hotels.com, and Anthem, to name a few. Why would sites as varied as those need access to a gyroscope or accelerometer? Like all modern development, websites are built upon layers and layers of libraries. Are they using the same one? Is some library several layers deep requesting access to an API it doesn't need?

I haven't figured it out yet, but here's what I've got - feel free to carry the torch.

An obfuscated file, possibly from Akamai

All the pages I've checked out reference an obfuscated file that, when removed, causes the motion sensor icon to disappear. The name is just a 112 bit random value that offers no clues.

  • Lowe's: c45ff2fedf18894428b6eae366abf1
  • FedEx: b6c65804238fde1fae4a597ae052
  • Anthem: c3ce05c96199f8c080a174ece11ff
  • ... and so on.

A look at the markup for the page shows it loads the script right before the end of the page, and it looks nearly identical in all cases.

Lowe's

<noscript>
    <img src="https://www.lowes.com/akam/11/pixel_49faa00b?a=dD1jMjczMGNkMmRlNmY0NDYwY2Q5MzQ2ZGVjNWI5YWIwZjEwZDM2Nzg0JmpzPW9mZg==" style="visibility: hidden; position: absolute; left: -999px; top: -999px;" />
</noscript>
<script type="text/javascript" >
    var _cf = _cf || []; 
    _cf.push(['_setFsp', true]);  
    _cf.push(['_setBm', true]); 
    _cf.push(['_setAu', '/resources/c45ff2fedf18894428b6eae366abf1']); 
</script>
<script type="text/javascript"  src="/resources/c45ff2fedf18894428b6eae366abf1"></script>

FedEx

<noscript>
    <img src="https://www.fedex.com/akam/11/pixel_190b4e7f?a=dD05YmZjNzQ1Njc1YTU3MDA5OWY0MDFiYjRmOWU3YTJhMzJjNjljNjdlJmpzPW9mZg==" style="visibility: hidden; position: absolute; left: -999px; top: -999px;" />
</noscript>
<script type="text/javascript" >
    var _cf = _cf || []; 
    _cf.push(['_setFsp', true]);  
    _cf.push(['_setBm', true]); 
    _cf.push(['_setAu', '/assets/b6c65804238fde1fae4a597ae052']); 
</script>
<script type="text/javascript"  src="/assets/b6c65804238fde1fae4a597ae052"></script>

Anthem

<noscript>
    <img src="https://www.anthem.com/akam/11/pixel_31f28831?a=dD04MTAwN2I4YzhlYmNjYjUzYTNjMzA2OTIyMjllNjYzOTRhYjRjNzFiJmpzPW9mZg==" style="visibility: hidden; position: absolute; left: -999px; top: -999px;" />
</noscript>
<script type="text/javascript" >
    var _cf = _cf || []; 
    _cf.push(['_setFsp', true]);  
    _cf.push(['_setBm', true]); 
    _cf.push(['_setAu', '/public/c3ce05c96199f8c080a174ece11ff']); 
</script>
<script type="text/javascript"  src="/public/c3ce05c96199f8c080a174ece11ff"></script>

Hotels.com

<script type="text/javascript" >
    var _cf = _cf || []; 
    _cf.push(['_setFsp', true]);  
    _cf.push(['_setBm', true]); 
    _cf.push(['_setAu', '/assets/0997d10d16655fda9826ab5d88ea']);
</script>
<script type="text/javascript"  src="/assets/0997d10d16655fda9826ab5d88ea"></script>

Kayo Sports

<noscript>
    <img src="https://kayosports.com.au/akam/11/pixel_52bf70fb?a=dD05ZWY4MzI1MTFjODFmMGFmYzhkZmUzNzhkZWNmM2RiZjVmYzc5ZWVhJmpzPW9mZg==" style="visibility: hidden; position: absolute; left: -999px; top: -999px;" />
</noscript>
<script type="text/javascript" >
    var _cf = _cf || []; 
    _cf.push(['_setFsp', true]);  
    _cf.push(['_setBm', true]); 
    _cf.push(['_setAu', '/assets/662c194b202d4b929be8d06c3195']); </script>
<script type="text/javascript"  src="/assets/662c194b202d4b929be8d06c3195"></script>

Since 4 of the 5 sites included a call to a URL with "akam/11/pixel" in it immediately prior, I assume it's related.. possibly some kind of tracking thing? A search of akam/11/pixel turns up loads of other sites that all cause the sensor icon to display too.

The randomly named js file is always the same, but obfuscated. I was able to deobfuscate it a bit using an online tool and then scripting a search and replace with that _ac array that has 711 elements in it, but that only gets you so far. Figuring out what this does will be a challenge, but searching for bits and pieces of code turned up a couple threads suggesting it's the Akamai bot detection service.

The values in the _ac array might have some clues in it, and some of the entries sound really suspicious. There's loads of references to various plugins, and no shortage of references to sensors (gyroscope, magnetometer, accelerometer and accelerationIncludingGravity, ambient-light-sensor, rotationRate, deviceorientation and DeviceOrientationEvent, DeviceMotionEvent, and sensor_data), and other odd stuff (startTracking and requestWakeLock).

var _ac = ["aj_indx_tact", "bind", "driver", "doe_cnt", "mousedown", "Batang", "clientHeight", "emit", "indexOf", "enReadDocUrl", "Palatino", "fpValstr", "onpointerup", "\"", "dm_en", "/get_params", "rVal", "return/*@cc_on!@*/!1", "-1,2,-94,-70,", "http://", "storage", "", "onkeypress", "navigator", "mn_update_challenge_details", "keydown", "spawn", "bm-telemetry", "y1", "-1,2,-94,-121,", "mediaDevices", "fillText", "mn_ct", "requestWakeLock", "Noto", "=", "//", "rotationRate", "call", "mn_mc_lmt", "toLowerCase", "uar", "Palatino-Bold", "arc", "-1,2,-94,-120,", "td", "unk", "z", "devicemotion", "gf", "push", ":", "TI-Nspire", "Edge PDF Viewer", "dme_cnt_lmt", "prototype", "hpu", "hku", "hkd", "mn_ts", "mn_tout", "Version/4.0", "non:", "sensor_data", "display", "getFloatVal", "sort", "ambient-light-sensor", "number", "~", "WebKit-integrierte PDF", "RTCPeerConnection", "0a46G5m17Vrp4o4c", "now", "value", "iPad;", "map", "GET", "mme_cnt", "msHidden", "vc", "||", "-1,2,-94,-124,", "-1", "x12:", "hf", "pstate", "callPhantom", "sd_debug", "RealPlayer Version Plugin", "lastIndexOf", "hpd", "floor", "XDomainRequest", "mn_lc", "clipboard-write", "pme_cnt", "vc_cnt_lmt", "AlNile", "shiftKey", "runFonts", "isIgn", "fpVal", "<@nv45. F1n63r,Pr1n71n6!", "hostname", "cka", "DeviceOrientationEvent", "mn_init", "accelerometer", "sqrt", "alpha", "applyFunc", "webkitRTCPeerConnection", "mduce_cnt_lmt", "attachEvent", "get_browser", "-1,2,-94,-116,", "is not a valid enum value of type PermissionName", "colorDepth", "dm_dis", "2d", "mn_h", "ke_vel", "$cdc_asdjflasutopfhvcZLmcfl_", "target", ",s7:", "Lobster", "touchcancel", "listFunctions", "-1,2,-94,-112,", ",s2:", "acos", "default", "ins", "-1,2,-94,-106,", "xagg", "visibilitychange", "hc", "pointerType", "Windows Media Player Plug-in Dynamic Link Library", "setRequestHeader", "mn_mc_indx", "Roboto", "vib:", "PLUGINS", "opc:", "get_cookie", "return a", "required", "dm:", "domAutomation", "midi", "onLine", "beta", "fillRect", "2", "informinfo", "keyCode", "doadma_en", "</setSDFN>", "dme_vel", "which", "hmu", "dmact", "==", "-1,2,-94,-119,", "aj_lmt_doact", "do_en", "appMinorVersion", "Mac OS X 10_5", "clearCache", "font", "getStorageUpdates", "Basic ", "x11:", "onblur", "monospace", "outerWidth", "htm", "Damascus", ",s3:", "isTrusted", "cookie_chk_read", "ff", "TouchEvent", "Bell MT", "altKey", "0", "-1,2,-94,-115,", "mozRTCPeerConnection", "pd", "tme_cnt", "hypot", "imul", "dis", "mn_il", "sf4", "hmm", "Google Earth Plug-in", "pact", "doe_cnt_lmt", "vibrate", "start_ts", "offsetHeight", "localStorage", "deviceorientation", "onmouseup", "join", "round", "height", "documentElement", "mozAlarms", "ceil", "Unity Player", "Java Plug-in 2 for NPAPI Browsers", "sc:", "doact", "Century Gothic", "geolocation", "none", "Birch Std", "enGetLoc", "mousemove", "tme_cnt_lmt", "6pt Arial", "toDataURL", "keyup", ",cpen:", "canvas", "Droid Serif", "tduce_cnt_lmt", "pluginInfo", "XMLHttpRequest", "name", "webkitHidden", "url", "mn_abck", "touchend", "afSbep8yjnZUjq3aL010jO15Sawj2VZfdYK8uY90uxq", "-1,2,-94,-109,", "#f60", "fromCharCode", "stroke", "Futura", "fpcf", "style", "bpd", "ver", "abs", "cc", "rgb(120, 186, 176)", "pe_cnt", "timezoneOffsetKey", "documentMode", "createElement", "webdriver", "Default Browser Helper", "nav_perm", "x", ",0", "undefined", "Minion Pro", "getTimezoneOffset", "webstore", "mact", "Shockwave Flash", "aj_lmt_dmact", "-", ",loc:", "storeWebWideTrackingException", "-1,2,-94,-117,", "Corsiva Hebrew", "doa_throttle", "rCFP", "hmd", "accelerationIncludingGravity", "message", "bd", "}", "hasIndexedDB", "permissions", "gd", "language", "asin", "pe_vel", "fontSize", "serviceWorker", "removeChild", "hvc", "cma", "sessionStorage", "apid.cformanalytics.com/api/v1/attempt", "magnetometer", "apicall_bm", "XPathResult", "sed", "default_session", "; ", "ke_cnt", "granted", "-1,2,-94,-80,", "metaKey", "params_url", "credentials", "Silverlight Plug-In", "{\"sensor_data\":\"", "https:", "<init/>", "readyState", "mn_al", "return ", "msManipulationViewsEnabled", "Mozilla Default Plug-in", "n_ck", "tst", "hasSessionStorage", "hts", "Shockwave for Director", "<setSDFN>", "onkeyup", "webrtcKey", "mozInnerScreenY", "parse_gp", "fonts", "query", "Century", "fillStyle", "undef", "startdoadma", "Apple Gothic", "pointerup", "disFpCalOnTimeout", "firstLoad", "t_dis", "sdfn", "webkitTemporaryStorage", "msvisibilitychange", "onmousedown", "Chrome PDF Viewer", "bc", "withCredentials", "init_time", "charCode", "DeviceMotionEvent", "cssText", "aj_indx", "pme_cnt_lmt", "toFixed", "function", "span", "Native Client", "fpValCalculated", "availHeight", "lang", "get_cf_date", "getElementById", "aj_ss", "document", "rir", "vcact", "me_cnt", "_", "mn_r", "hasLocalStorage", "get_mn_params_from_abck", "atanh", "getBattery", "Geneva", "mr", "mn_cd", "-1,2,-94,-103,", "fontFamily", "get_type", "body", "POST", "Calibri", "MSIE", "mozPhoneNumberService", "hkp", "ke_cnt_lmt", "input", "pageY", "pageX", "rgb(102, 204, 0)", "background-sync", "pen", "Buffer", "fonts_optm", "pointerdown", "plen", "userAgent", "offsetWidth", "-1,2,-94,-100,", "altFonts", "bluetooth", "mozvisibilitychange", "getforminfo", "_abck", "cbrt", "substring", "Authorization", "data", "Chrome Remote Desktop Viewer", "on", "fc:", "productSub", "touchmove", "search", "aj_indx_dmact", "clientX", "\\\'", "mduce_cnt", "denied", "clipboard-read", "getElementsByName", "QuickTime Plug-in", "div", "webkitGetGamepads", "parseInt", "exp", "isc:", "-1,2,-94,-118,", "doe_vel", "hidden", "childNodes", "Adobe Hebrew", "microphone", "-1,2,-94,-108,", "cdma", "button", "aj_indx_doact", "mn_get_current_challenges", "Java Applet Plug-in", "mouse", "clientWidth", "sendBeacon", "cache", "getGamepads", "Google Talk Plugin Video Renderer", "all", "onclick", "location", "Microsoft Office Live Plug-in", "state", "psub", "webkitvisibilitychange", "sessionStorageKey", "Fantasque Sans Mono", "mn_sen", "aj_lmt_tact", "AdobeAAMDetect", "Courier New", "fas", "hardwareConcurrency", "Papyrus", "Lato", "RealPlayer(tm) G2 LiveConnect-Enabled Plug-In (32-bit)", ";", "split", "innerHeight", "ckie", "-1,2,-94,-114,", "ce_js_post", "check_stop_protocol", "tduce_cnt", ",setSDFN:", "_phantom", "iPhotoPhotocast", "selenium", "persistent-storage", "pixelDepth", "gb", "requestMediaKeySystemAccess", "auth", "Cantarell", "; path=/; expires=Fri, 01 Feb 2025 08:00:00 GMT;", "/_bm/_data", "cf_url", "Helvetica Neue", "calc_fp", "mn_w", "/", "application/json", "slice", "1", "Courier", "cta", "t_en", "Adobe Braille", "Cambria", "off", "opera", "abcdefhijklmnopqrstuvxyz1234567890;+-.", "URL", "Constructor", "Adobe Acrobat", "Microsoft Sans Serif", "doNotTrack", "gamma", "o9", "-1,2,-94,-122,", "Open Sans", "</bpd>", "onpointerdown", "password", "set_cookie", "vc_cnt", "getdurl", "90px", "notifications", "dme_cnt", "onload", "mozHidden", "bm", "localStorageKey", "tact", "\"}", "d2", "\",\"sensor_data\" : \"", "updatet", "session_id", "splice", "ctrlKey", "then", "Avenir", "dma_throttle", "od", "mn_lcl", "New York", "indexedDbKey", "bdm", "charAt", "-1,2,-94,-105,", "den", "clipboard", "exception", "cns", "chknull", "ir", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", "hb", "random", "mozIsLocallyAvailable", "width", "ab", "pduce_cnt", "camera", "onmousemove", "rotate_left", "shift", "sans-serif", ",uaend,", "device-info", "me_vel", "keypress", "do_dis", "id", "Ubuntu Regular", "type", "string", "toString", "lvc", "wen", "payment-handler", "innerWidth", "mn_lg", "send", "onfocus", "strokeStyle", "iPhone", "htc", "prod", "Apple LiGothic", "mn_poll", "loc", "accessibility-events", "Monaco", "apply", "prevfid", "HTMLElement", "gyroscope", "x1", "javaEnabled", "te_vel", "SharePoint Browser Plug-in", "-1,2,-94,-102,", "activeElement", "https://", "Avenir Next", "wrc:", "startTracking", "kact", "d3", "screen", "mn_get_new_challenge_params", "to", "registerProtocolHandler", "-1,2,-94,-123,", "defaultValue", "a", "-1,2,-94,-101,", "Microsoft.XMLHTTP", "Times", "rve", "-1,2,-94,-111,", "product", "tel", "onreadystatechange", "np", "js_post", "protocol", "mozConnection", "email", "YouTube Plug-in", "text", "mn_stout", ",\"auth\" : \"", "Comic Neue", "mn_state", "-1,2,-94,-127,", "length", "cs", "getmr", "mn_pr", "PI", "pduce_cnt_lmt", "mme_cnt_lmt", "mn_psn", "m,Ev!xV67BaU> eh2m<f3AG3@", "enAddHidden", "-1,2,-94,-126,", "3", "cwen:", "getContext", "aj_type", "hte", "ta", "charCodeAt", "chrome", "getAttribute", "Content-type", "z1", "indexedDB", "clientY", "serif", "ArialHebrew-Light", "plugins", "get_stop_signals", "cookieEnabled", "i1:", "cdoa", "-1,2,-94,-110,", "Source Sans Pro", "pi", "getElementsByTagName", "bmisc", "te_cnt", "y", "addEventListener", ",it0", "mouseup", "x2", "speaker", "parse", "event", "patp", "fidcnt", "position: relative; left: -9999px; visibility: hidden; display: block !important", "\\\\\"", "{\"session_id\" : \"", "mn_ld", "cookie", "pow", "replace", "FileReader", "Oswald", ",s1:", "onkeydown", "acceleration", "catch", "touchstart", "api_public_key", "innerHTML", "toElement", "Ubuntu Medium", "autocomplete", "cpa", "prompt", "open", "mn_tcl", "Quicksand", "rst", "t_tst", "appendChild", "appVersion", "PointerEvent", "Widevine Content Decryption Module", "fsp", "bat:", "forEach", "WebEx64 General Plugin Container", "hn", "mn_cc", "16pt Arial", "availWidth", "click", "encode", "encode_utf8", "<bpd>", ",", ",mn_w:"];

Any requests to use those sensors, or even a check to see if a device supports them, would probably cause the sensor icon to show like it does.

The next 30 lines after that one have some possibly-interesting stuff too. A version number, some counters, a URL for some kind of analytics, an api key, and something (maybe a flag?) called sensor_data that's set to 0.

var _cf = _cf || [],
    bmak = bmak || {
        ver: 1.54,
        ke_cnt_lmt: 150,
        mme_cnt_lmt: 100,
        mduce_cnt_lmt: 75,
        pme_cnt_lmt: 25,
        pduce_cnt_lmt: 25,
        tme_cnt_lmt: 25,
        tduce_cnt_lmt: 25,
        doe_cnt_lmt: 10,
        dme_cnt_lmt: 10,
        vc_cnt_lmt: 100,
        doa_throttle: 0,
        dma_throttle: 0,
        session_id: default_session,
        js_post: !1,
        loc: ,
        cf_url: (https: === document[location][protocol] ? https:// : http://) + apid.cformanalytics.com/api/v1/attempt,
        params_url: (https: === document[location][protocol] ? https:// : http://) + document[location][hostname] + /get_params,
        auth: ,
        api_public_key: afSbep8yjnZUjq3aL010jO15Sawj2VZfdYK8uY90uxq,
        aj_lmt_doact: 1,
        aj_lmt_dmact: 1,
        aj_lmt_tact: 1,
        ce_js_post: 0,
        init_time: 0,
        informinfo: ,
        prevfid: -1,
        fidcnt: 0,
        sensor_data: 0,

A quick whois on cformanalytics.com suggests it belongs to Akamai. It could just mean some other service is using their API, but given the threads I found, I suspect it belongs to Akamai.

Domain Name: CFORMANALYTICS.COM
Registry Domain ID: 1897860898_DOMAIN_COM-VRSN
Registrar WHOIS Server: whois.akamai.com
Registrar URL: http://www.akamai.com
Updated Date: 2020-04-07T18:35:33Z
Creation Date: 2015-01-24T01:00:53Z
Registry Expiry Date: 2022-01-24T01:00:53Z
Registrar: Akamai Technologies, Inc.
Registrar IANA ID: 2480
Registrar Abuse Contact Email: registrar-abuse@akamai.com
Registrar Abuse Contact Phone: +1.6174443076

One function in that whole mess stands out particularly. It seems to be hitting tons of permissions to see if the browser prompts you for each one, or simply grants or denies them without prompting.

np: function () {
    var a = [],
        t = [geolocation, notifications, push, midi, camera, microphone, speaker, device-info, background-sync, bluetooth, persistent-storage, ambient-light-sensor, accelerometer, gyroscope, magnetometer, clipboard, accessibility-events, clipboard-read, clipboard-write, payment-handler];
    try {
        if (!navigator[permissions]) return 6;
        var e = function (t, e) {
                return navigator[permissions][query]({
                    name: t
                })[then](function (t) {
                    switch (t[state]) {
                    case prompt:
                        a[e] = 1;
                        break;
                    case granted:
                        a[e] = 2;
                        break;
                    case denied:
                        a[e] = 0;
                        break;
                    default:
                        a[e] = 5
                    }
                })[catch](function (t) {
                    a[e] = -1 !== t[message][indexOf](is not a valid enum value of type PermissionName) ? 4 : 3
                })
            },
            n = t[map](function (a, t) {
                return e(a, t)
            });
        Promise[all](n)[then](function () {
            bmak[nav_perm] = a[join]()
        })
    } catch (a) {
        return 7
    }
},

When the script is executed, one of the functions it runs is named startTracking, and runs bpd and in turn the above function np. I'm not sure why a bot detection service, if that's what it is, would need to check these, but maybe it's just one signal in a myriad of signals that they use to detect whether or not a requestor is a human or a bot? Or maybe it's being used as part of fingerprinting to track and individually identify visitors?

That's as far as I've gotten with unwinding this so far. Anyone else found anything interesting? Feel free to share below...

Author

Grant Winney

I write when I've got something to share - a personal project, a solution to a difficult problem, or just an idea. We learn by doing and sharing. We've all got something to contribute.


Comments / Reactions