@s4e/jsentinel
Version:
Script integrity and clipboard tamper detection for browsers.
5 lines (4 loc) • 4.77 kB
JavaScript
var x=new TextEncoder;async function S(i){let n=await crypto.subtle.digest("SHA-256",x.encode(i));return"sha256-"+btoa(String.fromCharCode(...new Uint8Array(n)))}function c(i){window.dispatchEvent(new CustomEvent("jsentinel",{detail:i,bubbles:!0,composed:!0}))}function b(i){return i.trim().replace(/\r\n/g,`
`).replace(/[ \t]+/g," ").replace(/[ \t]*\n[ \t]*/g,`
`)}function E(i,n){return!!((i.type||"").toLowerCase()==="application/ld+json"||/__NEXT_DATA__|self\.__next_|window\.__NUXT__/.test(n)||/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/.test(n))}function L(i){let n=new Map,l=new Set,e=new Set,f=[],a=new Map;for(let t of i){if("inline"in t){t.type==="inline-stable"&&t.hash&&l.add(t.hash);continue}if(t.type==="same-origin"&&t.hash)try{let r=new URL(t.src,window.location.href);n.set(r.pathname,t.hash),n.set(decodeURIComponent(r.pathname),t.hash),n.set(t.src,t.hash);let u=(r.pathname.split("/").pop()||"").toLowerCase();u&&a.set(u,t.hash)}catch{n.set(t.src,t.hash);let r=(t.src.split("/").pop()||"").toLowerCase();r&&a.set(r,t.hash)}else t.type==="versioned-cdn"?e.add(t.src):t.type==="dynamic-loader"&&f.push({host:t.host,path:t.path,query:t.query})}return{sameOriginHashes:n,inlineHashes:l,versionedPins:e,dynamicPins:f,filenameIndex:a}}function v(i,n){i&&window.dispatchEvent(new CustomEvent("jsentinel",{detail:{type:"script.checked",data:n},bubbles:!0,composed:!0}))}function C(i,n){try{let l=new URL(i,window.location.href);return n.some(e=>!(e.host&&l.host!==e.host||e.path&&l.pathname!==e.path||e.query&&l.search.replace(/^\?/,"")!==(e.query||"")))}catch{return!1}}async function k(i,n,l){try{if(!i.src){let s=i.textContent||"",o=b(s),d=await S(o),y=n.inlineHashes.has(d);if(v(l,{key:"inline",src:null,inline:!0,expected:y?d:void 0,got:d,ok:y}),y)return c({type:"script.ok",data:{key:"inline",src:null,inline:!0,expected:d,got:d,ok:!0,severity:"info"}}),!0;let g=E(i,s);return c({type:"script.mismatch",data:{key:"inline",src:null,inline:!0,expected:void 0,got:d,ok:!1,severity:g?"warn":"error",reason:g?"volatile-inline":"manifest-missing-key"}}),!1}let e=i.src,f=!1,a="";try{let s=new URL(e,window.location.href);f=s.origin===window.location.origin,a=s.pathname}catch{}if(f){let o=n.sameOriginHashes.get(a)||n.sameOriginHashes.get(decodeURIComponent(a))||n.sameOriginHashes.get(e);if(!o){let h=(a.split("/").pop()||"").toLowerCase();if(h){let w=n.filenameIndex.get(h);w&&(o=w)}}let d="",y=!1;try{let h=await(await fetch(e,{cache:"no-store"})).text();d=await S(h)}catch{y=!0}if(y)return c({type:"script.mismatch",data:{key:a||e,src:e,inline:!1,expected:o,got:"",ok:!1,severity:"error",reason:"fetch-error"}}),!1;let g=o===d;return v(l,{key:a||e,src:e,inline:!1,expected:o,got:d,ok:g}),g?(c({type:"script.ok",data:{key:a||e,src:e,inline:!1,expected:o,got:d,ok:!0,severity:"info"}}),!0):(c({type:"script.mismatch",data:{key:a||e,src:e,inline:!1,expected:o,got:d,ok:!1,severity:"error",reason:o?"hash-different":"manifest-missing-key"}}),!1)}if(!/^https:\/\//i.test(e))return c({type:"script.mismatch",data:{key:e,src:e,inline:!1,expected:void 0,got:"",ok:!1,severity:"warn",reason:"insecure-http"}}),!1;let t="",r=!1;try{let s=await(await fetch(e,{cache:"no-store"})).text();t=await S(s),r=!0}catch{}let u=n.versionedPins.has(e),m=C(e,n.dynamicPins);return!u&&!m?(c({type:"script.mismatch",data:{key:e,src:e,inline:!1,expected:void 0,got:r?t:"",ok:!1,severity:"warn",reason:"url-pin-mismatch"}}),!1):(v(l,{key:e,src:e,inline:!1,expected:void 0,got:r?t:"",ok:!0}),c({type:"script.ok",data:{key:e,src:e,inline:!1,expected:void 0,got:r?t:"",ok:!0,severity:"info"}}),!0)}catch(e){return c({type:"error",data:{stage:"check",message:String(e?.message||e)}}),!1}}async function M(i={}){let n=i.manifestPath??"/manifest.json",l=i.watchClipboard!==!1,e=!!i.verboseChecks;try{let a=await(await fetch(n,{cache:"no-store"})).json(),t=Array.isArray(a?.scripts)?a.scripts:[],r=L(t),u=Array.from(document.scripts),m=await Promise.all(u.map(p=>k(p,r,e)));if(c({type:"scan.complete",data:{total:m.length,ok:m.filter(Boolean).length,mismatched:m.filter(p=>!p).length}}),new MutationObserver(p=>p.flatMap(s=>Array.from(s.addedNodes)).filter(s=>s.nodeName==="SCRIPT").forEach(s=>{k(s,r,e)})).observe(document.documentElement,{childList:!0,subtree:!0}),l){let p="";document.addEventListener("selectionchange",()=>{p=window.getSelection()?.toString()||p}),document.addEventListener("copy",()=>{let s=p;setTimeout(async()=>{try{let o=await navigator.clipboard.readText();s&&o&&o!==s&&c({type:"clipboard.tamper",data:{expected:s,got:o}})}catch{c({type:"clipboard.permission",data:{}})}},100)})}c({type:"ready",data:{}})}catch(f){c({type:"error",data:{stage:"init",message:String(f?.message||f)}})}}typeof window<"u"&&typeof document<"u"&&M();
//# sourceMappingURL=auto.js.map