EV-WebKit
view release on metacpan or search on metacpan
wext/evwk_fp.c view on Meta::CPAN
* X is: claiming every negated clause dragged queries with no spoofed content
* at all into the wrapper, and delegating the whole `(not X)` instead answers
* a query form the engine may not accept. */
" var deleg=function(t){ var r=orig.call(window,t); return !!(r&&r.matches); };"
" var bal=function(t){ var d=0, i;"
" for(i=0;i<t.length;i++){ var c=t.charAt(i);"
" if(c==='(') d++;"
" else if(c===')'){ d--; if(d===0) return i===t.length-1; if(d<0) return false; } }"
" return false; };"
/* unit/cond recurse once per paren group and per `not`, on query text the
* page controls, so unbounded nesting overflows the JS stack and matchMedia
* throws a RangeError where a real engine simply answers. Two guards, and
* the ORDER of preference between them matters:
*
* First, collapse redundant parens. '((((X))))' is just '(X)', and
* collapsing costs one pass instead of a stack frame per level, so the
* cheap pathological case never approaches the cap and still gets the
* correct SPOOFED answer at any depth.
*
* Only a genuinely structured query -- hundreds of real nested conditions,
* which no page emits -- can still reach the cap. Such a breach must NOT be
* delegated to the engine: delegation was tried, and it re-opened the exact
* leak this evaluator exists to close, because the host answers a
* device-width probe with the REAL geometry. Measured on a pixel-chrome
* profile: a plain binary search returned the spoofed 412, the same search
* wrapped in 300 parens returned the host's true 1280. Answering false and
* claiming it as ours is a tell for an absurd query but discloses nothing,
* which is the right way round. */
" var MAXDEPTH=256, TOODEEP={v:false,s:true};"
" var unit=function(t,dep){"
" t=t.trim();"
" while(t.charAt(0)==='('&&bal(t)){"
" var q=t.slice(1,t.length-1).trim();"
" if(q.charAt(0)==='('&&bal(q)) t=q; else break; }"
" if(dep>MAXDEPTH) return TOODEEP;"
" var n=t.match(/^not\\b\\s*([\\s\\S]+)$/);"
" if(n){ var r=unit(n[1],dep+1); return {v:!r.v,s:r.s}; }"
" if(t.charAt(0)!=='('||!bal(t)) return {v:deleg(t),s:false};"
" var inner=t.slice(1,t.length-1).trim();"
" if(!inner) return {v:deleg(t),s:false};"
/* A parenthesised GROUP is a condition, not a feature test: evaluating it
* recursively is what makes '((A) or (B))' agree with 'A or B' instead of
* being handed to the host whole. */
" if(inner.charAt(0)==='('||/^not\\b/.test(inner)) return cond(inner,dep+1);"
" var fv=featVal(inner);"
" return fv===null?{v:deleg(t),s:false}:{v:fv,s:true}; };"
" var cond=function(s,dep){"
" var ps=splitTop(s,/^\\s*\\bor\\b\\s*/), i, r, v, sp=false;"
" if(ps.length>1){ v=false;"
" for(i=0;i<ps.length;i++){ r=unit(ps[i],dep+1); if(r.s) sp=true; if(r.v) v=true; }"
" return {v:v,s:sp}; }"
" ps=splitTop(s,/^\\s+and\\s+/);"
" if(ps.length>1){ v=true;"
" for(i=0;i<ps.length;i++){ r=unit(ps[i],dep+1); if(r.s) sp=true; if(!r.v) v=false; }"
" return {v:v,s:sp}; }"
" return unit(s,dep); };"
/* Evaluate a COMPOUND query branch by branch and clause by clause: a spoofed
* clause uses our answer, any other is delegated to the real engine and the
* results combined. Matching whole literal strings meant
* '(pointer:coarse) and (min-width:1px)' and comma lists silently reported the
* real desktop answer. */
" var evalQuery=function(lq){"
" var ors=splitTop(lq, /^\\s*(?:,|\\bor\\b)\\s*/), spoofed=false, result=false, i, j;"
" for(i=0;i<ors.length;i++){"
/* `only <type>` is semantically identical to `<type>` (the keyword only hides
* the query from prehistoric parsers), and `not` negates the whole branch.
* Treating either as unparseable abandoned the spoof and returned the
* contradicting host answer for the most common real-world form. */
" var branch=ors[i].trim(), negate=false;"
" if(/^only\\s+/.test(branch)) branch=branch.replace(/^only\\s+/,'');"
" else if(/^not\\s+/.test(branch)){ negate=true; branch=branch.replace(/^not\\s+/,''); }"
" var parts=splitTop(branch, /^\\s+and\\s+/), all=true, usable=true, got=[];"
" for(j=0;j<parts.length;j++){"
" var c=parts[j].trim();"
" if(!c){ usable=false; break; }"
" if(c.charAt(0)!=='('){"
" if(c==='screen'||c==='all') continue;" /* always true in a screen browsing context */
" usable=false; break; }" /* print/speech/... -> let the host answer */
" got.push(c); }"
/* A branch we cannot tokenize is delegated ON ITS OWN and OR-ed in. Bailing
* out for the whole query let one unhandleable alternative disable every
* spoofed one beside it: appending ', print' to any query handed the real
* device's answer back, reopening the geometry leak in the very form the
* tests pin. A disjunction is per-branch, so treat it that way. */
" if(!usable){"
" var um=orig.call(window, ors[i]);"
" if(um && um.matches) result=true;"
" continue; }"
" for(j=0;j<got.length;j++){ var rv=unit(got[j],0); if(rv.s) spoofed=true; if(!rv.v) all=false; }"
" if(negate) all=!all;"
" if(all) result=true; }"
" return spoofed ? result : null; };"
" var orig=window.matchMedia;"
/* Redefine `matches` ONCE on MediaQueryList.prototype, backed by a WeakMap of
* spoofed instances. A Proxy minted a fresh bound function per access (so
* m.addEventListener !== m.addEventListener) and failed the platform-object
* brand check; an own property on the instance would instead show up under
* getOwnPropertyDescriptor/hasOwnProperty where the real one is inherited.
* A prototype accessor has neither tell. */
" var mqOver=new WeakMap();"
" (function(){ var M=window.MediaQueryList; if(!M||!M.prototype) return;"
" var d=Object.getOwnPropertyDescriptor(M.prototype,'matches'); if(!d||!d.get) return;"
" var og=d.get;"
/* Store the QUERY and re-evaluate on every read. Caching the boolean froze a
* spoofed MediaQueryList at its creation-time answer, so a retained object
* contradicted a freshly created one for the same string and `change` never
* fired -- responsive pages stuck at their initial breakpoint. */
" var g=({ get matches(){ var q=mqOver.get(this);"
" if(q===undefined) return og.call(this);"
" var ev=evalQuery(q); return ev===null ? og.call(this) : ev; } });"
" var gd=Object.getOwnPropertyDescriptor(g,'matches').get;"
" Object.defineProperty(M.prototype,'matches',{enumerable:d.enumerable,configurable:true,"
" get:gd}); })();"
/* The engine serialises an unparseable query to media "not all", and such a
* MediaQueryList can NEVER match. Our looser tokenizer accepts some of those
* (e.g. 'only (pointer:coarse)' -- `only` requires a media type; or a
* trailing type), so without this check we reported matches:true beside
* media:"not all", a self-contradiction inside one object. */
" var mk=function(q,nq){"
" var real=orig.call(window,q);"
" try{ if(real && real.media==='not all') return real; }catch(e){}"
" try{ mqOver.set(real, nq); }catch(e){}"
" return real; };"
" var w=({ matchMedia(query){"
" if(arguments.length===0) throw new TypeError(\"Failed to execute 'matchMedia' on 'Window': 1 argument required, but only 0 present.\");"
" var q=query, lq=lite(q);"
" if(evalQuery(lq)!==null) return mk(q,lq);" /* spoofed: re-evaluated live on each .matches read */
" return orig.call(window,q);"
" }}).matchMedia;"
" window.matchMedia=w;"
" }"
/* Event-handler IDL attributes are ALWAYS accessors; a writable data property
* is a one-line tell on the two properties mobile detection reads first. */
/* All four touch handlers or none: no real touch-capable browser exposes
* ontouchstart alone, and a partial set beside maxTouchPoints>0 is a
* contradiction. */
" if(cfg.touch){"
" ['ontouchstart','ontouchend','ontouchmove','ontouchcancel'].forEach(function(h){"
" try{ if(h in window) return; var slot=null;"
" Object.defineProperty(window,h,{enumerable:true,configurable:true,"
" get(){ return slot; },"
" set(v){ slot = ((typeof v==='object'&&v!==null)||typeof v==='function') ? v : null; }}); }catch(e){} });"
" }"
" if(cfg.orientation){"
/* window.orientation is MOBILE-ONLY (and legacy); screen.orientation is
* universal. Emitting the orientation config for desktop profiles so they
* get screen.orientation must not also hand them window.orientation beside
* maxTouchPoints:0 and pointer:fine. Gate it on touch, and give it the
* onorientationchange sibling no real mobile browser omits. */
" if(cfg.touch){"
" try{ Object.defineProperty(window,'orientation',{enumerable:true,configurable:true,"
" get(){ return cfg.orientation.angle; }}); }catch(e){}"
" try{ if(!('onorientationchange' in window)){ var _ooc=null;"
" Object.defineProperty(window,'onorientationchange',{enumerable:true,configurable:true,"
" get(){ return _ooc; },"
" set(v){ _ooc=((typeof v==='object'&&v!==null)||typeof v==='function')?v:null; }}); } }catch(e){}"
" }"
" try{ var so=window.screen&&window.screen.orientation;"
" if(so){ var sp=Object.getPrototypeOf(so);" /* redefine on the existing ScreenOrientation.prototype */
" Object.defineProperty(sp,'type',{get(){return cfg.orientation.type;},enumerable:true,configurable:true});"
" Object.defineProperty(sp,'angle',{get(){return cfg.orientation.angle;},enumerable:true,configurable:true});"
" } else if(window.screen){" /* WebKitGTK exposes no screen.orientation -- stub it (mobile has one) */
/* Shaped as a real WebIDL interface for the same reasons NavigatorUAData was:
* a bare literal reported [object Object], exposed 8 own enumerable keys
* (real: none), was not an EventTarget, and left window.ScreenOrientation
* undefined -- and it sat as an own property of `screen` rather than an
* accessor on Screen.prototype. */
" var SO=function ScreenOrientation(){ throw new TypeError('Illegal constructor'); };"
" Object.setPrototypeOf(SO, EventTarget);"
" SO.prototype=Object.create(EventTarget.prototype,{constructor:{value:SO,writable:true,configurable:true}});"
" try{ Object.defineProperty(SO.prototype,Symbol.toStringTag,{value:'ScreenOrientation',configurable:true}); }catch(e){}"
" Object.defineProperty(SO.prototype,'type',{get(){ return cfg.orientation.type; },enumerable:true,configurable:true});"
" Object.defineProperty(SO.prototype,'angle',{get(){ return cfg.orientation.angle; },enumerable:true,configurable:true});"
" var _oc=null;"
" Object.defineProperty(SO.prototype,'onchange',{enumerable:true,configurable:true,"
" get(){ return _oc; }, set(v){ _oc=((typeof v==='object'&&v!==null)||typeof v==='function')?v:null; }});"
" var soM={ lock(){ return Promise.reject(new DOMException('lock not available','NotSupportedError')); }, unlock(){} };"
" Object.defineProperty(SO.prototype,'lock',{value:soM.lock,writable:true,enumerable:true,configurable:true});"
" Object.defineProperty(SO.prototype,'unlock',{value:soM.unlock,writable:true,enumerable:true,configurable:true});"
" try{ Object.defineProperty(SO,'prototype',{writable:false}); }catch(e){}"
" try{ if(!('ScreenOrientation' in window)) Object.defineProperty(window,'ScreenOrientation',{value:SO,writable:true,enumerable:false,configurable:true}); }catch(e){}"
" var soInst=Reflect.construct(EventTarget, [], SO);"
" var sproto=window.Screen ? window.Screen.prototype : window.screen;"
" Object.defineProperty(sproto,'orientation',{get(){ return soInst; },enumerable:true,configurable:true});"
" } }catch(e){}"
" }"
"})();";
/* DOM feature-presence stubs. Each is installed ONLY if the target does not
* already expose it (in-guarded), so a WebKitGTK build that ships a real API
* keeps the real one. Functional where feasible -- but there is no real ICE /
* device access behind them, and like the other JS-installed layers they are
* Function.prototype.toString.call-detectable (documented ceiling). Driven by
* cfg.features, an array of group names chosen per profile in Fingerprint.pm. */
/* navigator.plugins / mimeTypes / pdfViewerEnabled for a profile with NO inline
* PDF viewer -- currently only Chrome for Android, which had none at 131.
*
* The HTML spec hardcodes both states: a browser with inline PDF viewing reports
* pdfViewerEnabled true and five fixed plugin names, one without reports false
* and EMPTY lists. WebKitGTK reports the viewer-present state, so a Chrome 131
* Android profile contradicted itself in one property read.
*
* Faking the empty list is fiddly for one reason: PluginArray.prototype.length
* (and item/namedItem) BRAND-CHECK -- calling them on an Object.create(
* PluginArray.prototype) throws TypeError, so the obvious fake breaks on the
* first length read. Nor can length be shadowed as an own property: on a real
( run in 1.455 second using v1.01-cache-2.11-cpan-b16cb0d3907 )