Is HaramBlur - Blur Haram & NSFW Images & Videos safe?

Medium risk

HaramBlur is medium risk. Dynamic analysis captured HaramBlur rebuilding a diagnostics payload on every settings change and startup, encrypting it with a plaintext-passphrase key, embedded in a Google Form set as the uninstall URL; it reaches Google on page load.…

Mohamed Alganzoryv0.6.11Chrome Web Store
52Risk
Who publishes it

Mohamed Alganzory - 1 other listing from the same operator, none carrying a finding

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Mohamed Alganzory

Same store account

1 other listing published from this account, 30k+ users between them, none of them carrying a finding.

Shared hosts - 1 hostname

Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.

dnsforfamily.com
Also called by 1 other listing: HaramBlur - Blur Haram & NSFW Images & Videos

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Uninstall URL encodes browser fingerprint and settings to Google Forms

Dynamic analysis captured HaramBlur rebuilding a diagnostics payload on every settings change and startup, encrypting it with a plaintext-passphrase key, embedded in a Google Form set as the uninstall URL; it reaches Google on page load.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You change any HaramBlur setting, or the extension starts up in your browser.

This covers toggling blur options, updating your whitelist, or the extension loading after a browser restart, update, or fresh install.

The extension did this

The extension rebuilds an encrypted diagnostics payload and registers it as the URL Chrome will open when you uninstall the extension.

The registration happens well before any uninstall -- it re-arms on every qualifying event so the pending URL always reflects your current profile.

Why you can't catch this in DevTools

The diagnostics payload is AES-GCM encrypted with a key derived from a passphrase string that ships in plaintext inside the extension's own background.js bundle, so it can be decrypted by anyone who has the extension's code. This keeps the payload unreadable at a glance in the URL bar, but it is not a real confidentiality control.

Decoded value
ev=0.6.10;bn=chrome;bp=linux;bl=en-US;bg=en-US;hc=10;dm=16;mo=0;fi=1784663474235;ii=0;se=1;si=1;sv=1;sm=1;sf=1;ui=0;uv=0;sb=1;sg=0;ss=0;sh=0;sp=0;sc=0;su=0;pg=0;cm=0;wc=0;ba=30;st=0.4;th=0.4;md=vZNq2WHrFm7b;pm=free;au=0;ap=0;at=0;rc=0;ta=0;de=0;mm=vZNq2WHrFm7b;bo=0;be=0
Selected fields from the decrypted diagnostics payload (full payload has ~50 fields)
  • Device hardware profile
    10 CPU cores, 16 GB memory

    Combines your CPU core count and installed memory into a fingerprint that narrows down which device you're using.

  • Browser, platform, and language
    chrome, linux, en-US

    Identifies your browser vendor, operating system, and language/locale, adding further detail to your device fingerprint.

  • Extension install timestamp
    1784663474235 (2026-07-21T19:51:14Z)

    Records the exact millisecond you first installed the extension.

  • Full settings profile
    blur_male=true, blur_female=true, blur_amount=30, strictness=0.4, whitelist_count=0, password_protection_enabled=false

    Every blur, whitelist, and protection setting configured, including password protection status and whitelisted site count.

  • Account and subscription state
    is_authenticated=false, is_premium=false, is_trial_active=false, protection_mode=free

    Whether you're signed in, on a paid plan, or in a trial period.

  • Crash and detection-model telemetry
    crash_count=0, detection_errors=0, benchmark_model=vZNq2WHrFm7b

    Local crash counts and which on-device image-detection model your install last benchmarked.

The code that does this

Diagnostics assembly, AES-GCM encryption, and the trigger re-arming the uninstall URL

Readable version

Diagnostics payload assembly, encryption, and uninstall URL builder

dist/background.js
_e="hb1",Te="hb-uninstall-v1-s6G7X9r2Q1m8Lp4Na0TzK5wV3cY7Df2H",R=null;function Q(){if(!globalThis.crypto?.subtle)throw new Error("Web Crypto API is unavailable");return globalThis.crypto}async function ke(){if(!R){let t=Q(),e=new TextEncoder().encode(Te),r=await t.subtle.digest("SHA-256",e);R=t.subtle.importKey("raw",r,{name:"AES-GCM"},!1,["encrypt","decrypt"])}return R}function j(t){let e=Array.from(t,s=>String.fromCharCode(s)).join("");return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/u,"")}function Ce(t){return t==null||t===""?"":typeof t=="boolean"?t:Array.isArray(t)?t.filter(e=>e!=null&&e!=="").join(","):String(t).trim()}function Me(t,e){let r=Ce(e);return r===""?"":Ie.has(t)?r===!0||r===1||r==="1"||r==="true"?"1":"0":encodeURIComponent(String(r))}function Y(t){return q.map(([e,r])=>{let s=Me(e,t[r]);return s===""?null:`${e}=${s}`}).filter(Boolean).join(";")}async function X(t){let e=String(t||"").trim();if(!e)return"";let r=Q(),s=r.getRandomValues(new Uint8Array(12)),n=await ke(),o=new TextEncoder().encode(e),a=new Uint8Array(await r.subtle.encrypt({name:"AES-GCM",iv:s},n,o));return`${_e}.${j(s)}.${j(a)}`}var De=new Set(["0.6.10"]);function J(t,e){return t===e||t==="0.6.8"?{continueInitialization:!1,showUpdateTour:!1}:{continueInitialization:!0,showUpdateTour:!De.has(e)}}var E=G,K=0,O=0,C=!1,M=!1,ee="authStateValidation",te="customerStateSync",re="trialExpirationCheck",x="installMetadata",P="uninstallDiagnosticsRuntime",Ue=1023,B="https://docs.google.com/forms/d/e/1FAIpQLSeKaTN6sOZkMUFLgfQJLHZadm3_mL_QYeq9dFKaF6frUkFICQ/viewform",Re="1257235115",L=!1,N=!1;function Oe(){let t=navigator.userAgentData?.brands||[];for(let r of t){if(/edge/i.test(r.brand))return"edge";if(/opera/i.test(r.brand))return"opera";if(/chrome/i.test(r.brand))return"chrome"}let e=navigator.userAgent||"";return/edg/i.test(e)?"edge":/opr\//i.test(e)?"opera":/firefox/i.test(e)?"firefox":/safari/i.test(e)&&!/chrome|chromium|edg/i.test(e)?"safari":/chrome|chromium/i.test(e)?"chrome":"unknown"}function Le(){let e=(navigator.userAgentData?.platform||navigator.platform||"").toLowerCase();return e.includes("mac")?"macos":e.includes("win")?"windows":e.includes("linux")?"linux":e.includes("android")?"android":e.includes("iphone")||e.includes("ipad")?"ios":e||"unknown"}function Ne(){return new Promise(t=>{if(!chrome.extension?.isAllowedIncognitoAccess){t(null);return}chrome.extension.isAllowedIncognitoAccess(e=>{t(!!e)})})}async function Pe(){return{browserName:Oe(),platformName:Le(),language:navigator.language||"",languages:Array.isArray(navigator.languages)?navigator.languages:[],hardwareConcurrency:navigator.hardwareConcurrency??"",deviceMemory:navigator.deviceMemory??"",mobile:navigator.userAgentData?.mobile??!1,incognitoAccessAllowed:await Ne()}}async function H(){let{[x]:t}=await chrome.storage.local.get(x),e={firstInstalledAt:t?.firstInstalledAt||Date.now()};return e.firstInstalledAt!==t?.firstInstalledAt&&await chrome.storage.local.set({[x]:e}),e}function Be(t){let e=new URLSearchParams({usp:"pp_url"});t&&e.set(`entry.${Re}`,t);let r=`${B}?${e.toString()}`;return r.length>Ue?(console.warn("Uninstall URL exceeded browser limit; falling back to Google Form without diagnostics",r.length),B):r}async function He(){let t=chrome.runtime.getManifest(),e=await chrome.storage.sync.get(["hb-settings","hb-auth-user","hb-allowed-sites"]),r=await chrome.storage.local.get(["crashInfo","turboModeStatus",P,x]),s=await Pe(),n=r[x]||await H(),o={...E,...e["hb-settings"]||{}},a=e["hb-auth-user"]||{},i=e["hb-allowed-sites"]||[],u=r.crashInfo||{},d=r.turboModeStatus||{},c=r[P]||{},l=c.benchmark||{},m=new Set([...Array.isArray(o.whitelist)?o.whitelist:[],...Array.isArray(i)?i:[]]).size,y={"extension.version":t.version,"browser.name":s.browserName,"browser.platform":s.platformName,"browser.language":s.language,"browser.languages":s.languages.join(","),"browser.hardware_concurrency":s.hardwareConcurrency,"browser.device_memory_gb":s.deviceMemory,"browser.mobile":s.mobile,"install.first_installed_at":n.firstInstalledAt||"","install.incognito_access_allowed":s.incognitoAccessAllowed,"settings.extension_enabled":o.status,"settings.blur_images":o.blurImages,"settings.blur_videos":o.blurVideos,"settings.blur_male":o.blurMale,"settings.blur_female":o.blurFemale,"settings.unblur_images":o.unblurImages,"settings.unblur_videos":o.unblurVideos,"settings.specific_blur":o.specificBlur,"settings.gray_mode":o.gray,"settings.use_solid_color":o.useSolidColor,"settings.hide_video_toggle":o.hideVideoToggle,"settings.password_protection_enabled":o.passwordProtectionEnabled,"settings.companion_enabled":o.companionMode?.enabled,"settings.uninstall_prevention_enabled":o.uninstallPreventionEnabled,"settings.prevent_incognito_guest_enabled":o.preventIncognitoGuestEnabled,"settings.compare_mode_enabled":o.compareMode?.enabled,"settings.whitelist_count":m,"settings.blur_amount":o.blurAmount,"settings.strictness":o.strictness,"settings.score_threshold":o.scoreThreshold,"settings.detection_model":o.detectionModel,"settings.protection_mode":o.protectionMode,"account.is_authenticated":!!a?.uid,"account.is_premium":!!a?.isPremium,"account.is_trial_active":!!a?.isTrialActive,"runtime.crash_count":u.crashesCount||0,"runtime.crash_timestamp":u.crashTimestamp||"","runtime.turbo_active":!!d.active,"runtime.turbo_error":d.error||"","runtime.active_backend":c.activeBackend||"","runtime.saved_texture_limit":c.savedTextureLimit||"","runtime.detection_errors":c.detectionErrors||0,"runtime.last_detection_error_at":c.lastDetectionErrorAt||"","runtime.model_error_at":c.modelErrorAt||"","runtime.last_recreation_attempt_at":c.lastRecreationAttemptAt||"","benchmark.selected_model":l.selectedModel??o.detectionModel,"benchmark.average_time_ms":l.averageTimeMs??"","benchmark.sample_count":l.sampleCount??"","benchmark.raw_sample_count":l.rawSampleCount??"","benchmark.timeout":!!l.timeout,"benchmark.early_exit":!!l.earlyExit,"benchmark.recorded_at":l.recordedAt||"","benchmark.error":l.error||""};return{diagnosticsPayload:Y(y)}}async function We(){try{let t=await He(),e=await X(t.diagnosticsPayload);await chrome.runtime.setUninstallURL(Be(e))}catch(t){console.error("Failed to update uninstall URL:",t);try{await chrome.runtime.setUninstallURL(B)}catch(e){console.error("Failed to set fallback uninstall URL:",e)}}}async function W(){if(N=!0,!L){L=!0;try{for(;N;)N=!1,await We()}finally{L=!1}}}

Storage-change listener that re-fires the uninstall URL update

dist/background.js
chrome.storage.onChanged.addListener(async(t,e)=>{if(e==="sync"&&t["hb-settings"]){let s=t["hb-settings"].newValue,n=t["hb-settings"].oldValue,o=s?.companionMode,a=n?.companionMode;if(o?.enabled&&o?.email){let i=Date.now(),u=o.lastNotificationTime||0,d=30*1e3;if(i-u>d){if(!await h.isPremiumUser()){console.error("Companion notification attempted without premium access");let f={...s};f.companionMode={...o,enabled:!1},chrome.storage.sync.set({"hb-settings":f});return}let l={},m=["detectionModel","version","trialWelcomeShown"];for(let f in s)!m.includes(f)&&JSON.stringify(s[f])!==JSON.stringify(n?.[f])&&(l[f]={old:n?.[f],new:s[f]});let y=JSON.stringify(o)!==JSON.stringify(a);if(Object.keys(l).length>0&&!y)try{await h.sendCompanionNotification({email:o.email,modifications:l,url:"Extension Settings"});let f={...s};f.companionMode={...o,lastNotificationTime:i},chrome.storage.sync.set({"hb-settings":f})}catch(f){console.error("Error sending companion notification:",f)}}}}(e==="sync"&&(t["hb-settings"]||t["hb-auth-user"]||t["hb-allowed-sites"])||e==="local"&&(t.crashInfo||t.turboModeStatus||t[P]||t[x]))&&await W()})
Check if you're affected

Decrypts a captured HaramBlur uninstall-diagnostics token using the same AES-256-GCM key the extension derives from its own hardcoded passphrase, recovering the plaintext diagnostics string.

Requires
  • Node.js 18+ (uses the built-in node:crypto module)
hb-uninstall-decode.js · js
#!/usr/bin/env node// Decrypts a HaramBlur uninstall-diagnostics ciphertext ("hb1.<iv>.<ciphertext>")// using the AES-256-GCM key derived from the passphrase hardcoded in the// extension's shipped background.js bundle.//// Usage: node hb-uninstall-decode.js "hb1.<iv_b64url>.<ciphertext_b64url>"const crypto = require("node:crypto");// Hardcoded passphrase shipped in dist/background.js (variable `Te`).const HARDCODED_PASSPHRASE = "hb-uninstall-v1-s6G7X9r2Q1m8Lp4Na0TzK5wV3cY7Df2H";function b64urlToBuffer(s) {  const padded = s.replace(/-/g, "+").replace(/_/g, "/");  const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4));  return Buffer.from(padded + pad, "base64");}function decode(token) {  const [prefix, ivB64, ctB64] = token.split(".");  if (prefix !== "hb1") throw new Error(`Unexpected prefix: ${prefix}`);  const key = crypto.createHash("sha256").update(HARDCODED_PASSPHRASE).digest();  const iv = b64urlToBuffer(ivB64);  const ctWithTag = b64urlToBuffer(ctB64);  // Web Crypto's AES-GCM appends the 16-byte auth tag to the ciphertext.  const tag = ctWithTag.subarray(ctWithTag.length - 16);  const ciphertext = ctWithTag.subarray(0, ctWithTag.length - 16);  const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);  decipher.setAuthTag(tag);  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);  return plaintext.toString("utf8");}const token = process.argv[2];if (!token) {  console.error("Usage: node hb-uninstall-decode.js <entry.1257235115 value>");  process.exit(1);}console.log(decode(token));
How to run it
  1. 1Capture entry.1257235115 from the pending uninstall URL (chrome.management, or observe setUninstallURL in a test profile).
  2. 2Run: node hb-uninstall-decode.js "hb1.<iv>.<ciphertext>".
  3. 3It prints the decrypted diagnostics string.
External destination used by the uninstall-diagnostics link
    • docs.google.com

    Receives the diagnostics ciphertext as a URL query parameter the moment the pre-armed Google Forms link loads, which happens automatically when the extension is uninstalled.

Image report sends settings JSON to Google Forms

Submitting an image report posts the report choice, page URL, displayed/original image URLs, and the settings JSON to a Google Forms endpoint.

Settings can include blur preferences, whitelist entries, password state, and companion email.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-200
Source
Dynamic sandbox
What actually happens
You did this

You submit an image report from the extension modal.

The modal offers false-positive and false-negative choices before enabling Send Report.

The extension did this

The extension sends the report choice, page URL, image URLs, and saved settings JSON to Google Forms.

Captured request
POSThttps://docs.google.com/forms/u/0/d/e/1FAIpQLSfHbTsJaydX__sCbv7aGf0RiVBxTcbBSzv5LU-VALJm1TFnlg/formResponse
Fields assembled for the Google Forms submission
  • Report choice
    false-positive

    Shows whether you marked the image as wrongly blurred or missed by the filter.

  • Displayed image URL
    https://cdn.example.org/gallery/photo-4812.jpg (illustrative)

    Identifies the image shown in the report modal.

  • Original image URL
    https://images.example.net/originals/photo-4812.jpg (illustrative)

    Can identify the image source before the extension replaced or processed it.

  • Current page URL
    https://example.com/gallery/article-17 (illustrative)

    Identifies the page where you submitted the image report.

  • Saved extension settings
    {"blurAmount":30,"strictness":0.4,"whitelist":["example.com"],"passwordProtectionEnabled":false,"companionMode":{"enabled":true,"email":"parent@example.com"}} (illustrative)

    Can reveal your extension configuration, including whitelist entries, blur choices, and companion-mode email settings.

The code that does this

Report handler builds URLSearchParams and posts them to Google Forms

Readable version

Deobfuscated report handler

dist/background.js
function fe(s,e,t){chrome.scripting.executeScript({target:{tabId:s.id},func:(r,o,a,n)=>{let i=document.getElementById("hb-report-modal");i&&i.remove();let c=`        <div class="hb-report-modal" dir="ltr">          <div class="hb-modal-content">            <div class="hb-modal-header">              <div class="hb-header-title">                <img src="${chrome.runtime.getURL("src/assets/hb-icon-48.png")}" width="24" height="24" alt="HaramBlur">                <h3>Report Image</h3>              </div>              <button class="hb-close-button">&times;</button>            </div>            <div class="hb-modal-body">              <p class="hb-description">Help us improve our detection by reporting any issues.</p>              <div class="hb-image-section">                <button class="hb-toggle-image hb-button hb-button-secondary">                  <span class="hb-toggle-text">Show Image</span>                  <span class="hb-toggle-arrow">\u25B6</span>                </button>                <div class="hb-image-preview" style="display: none;">                  <img src="${r}" alt="Content preview">                </div>              </div>              <div class="hb-report-options">                <p class="hb-label">What's wrong with this detection?</p>                <label class="hb-radio-label">                  <input type="radio" name="report-type" value="false-positive">                  <span>False Positive - Content was wrongly blurred</span>                </label>                <label class="hb-radio-label">                  <input type="radio" name="report-type" value="false-negative">                  <span>False Negative - Inappropriate content wasn't blurred</span>                </label>              </div>            </div>            <div class="hb-modal-footer">              <button class="hb-button hb-button-secondary hb-cancel-button">Cancel</button>              <button class="hb-button hb-button-primary hb-submit-button" disabled>Send Report</button>            </div>          </div>        </div>      `,h=document.createElement("div");h.id="hb-report-modal";let l=h.attachShadow({mode:"closed"}),u=document.createElement("style");u.textContent=`        /* CSS Reset for Shadow DOM */        :host {          all: initial;          display: block;        }                * {          margin: 0;          padding: 0;          box-sizing: border-box;          font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;          line-height: 1.5;        }                .hb-report-modal {          position: fixed;          inset: 0;          background: rgba(0, 0, 0, 0.5);          display: flex;          align-items: center;          justify-content: center;          z-index: 999999;          font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;          color: #333;          font-size: 14px;          font-weight: normal;          text-align: left;          letter-spacing: normal;          word-spacing: normal;          text-transform: none;          text-indent: 0;          text-shadow: none;          text-decoration: none;        }        .hb-modal-content {          background: white;          background-color: white;          border-radius: 8px;          width: 90%;          max-width: 500px;          max-height: 90vh;          overflow: auto;          box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);          color: #333;          font-size: 14px;        }        .hb-modal-header {          padding: 16px;          border-bottom: 1px solid #eee;          display: flex;          justify-content: space-between;          align-items: center;        }        .hb-header-title {          display: flex;          align-items: center;          gap: 8px;        }        .hb-header-title h3 {          margin: 0;          font-size: 18px;          font-weight: 600;          color: #333;          line-height: 1.2;          text-transform: none;          letter-spacing: normal;        }        .hb-close-button {          border: none;          background: none;          font-size: 24px;          cursor: pointer;          padding: 4px;          opacity: 0.6;        }        .hb-close-button:hover {          opacity: 1;        }        .hb-modal-body {          padding: 16px;        }        .hb-description {          margin: 0 0 16px;          opacity: 0.7;          font-size: 14px;          color: #666;          font-weight: normal;          line-height: 1.5;        }         .hb-radio-label {          display: flex;          align-items: flex-start;          padding: 8px 0;          cursor: pointer;          gap: 12px;          position: relative;        }        .hb-radio-label input[type="radio"] {          -webkit-appearance: none;          appearance: none;          margin: 0;          width: 20px;          height: 20px;          border: 2px solid #ccc;          border-radius: 50%;          flex-shrink: 0;          margin-top: 2px;          position: relative;          transition: all 0.2s ease;        }        .hb-radio-label:hover input[type="radio"] {          border-color: #2196f3;        }        .hb-radio-label input[type="radio"]:checked {          border-color: #2196f3;          border-width: 6px;          background: white;        }        .hb-radio-label span {          font-size: 14px;          color: #444;          line-height: 1.4;          padding-top: 2px;        }        .hb-report-options {          margin-bottom: 20px;          display: flex;          flex-direction: column;          gap: 8px;        }        .hb-label {          font-weight: 500;          margin-bottom: 8px;          color: #333;          font-size: 14px;          display: block;          line-height: 1.5;        }        .hb-image-section {          margin-bottom: 16px;        }        .hb-toggle-image {          width: 100%;          display: flex;          justify-content: space-between;          align-items: center;          padding: 12px 16px;          font-size: 14px;          margin-bottom: 8px;        }        .hb-toggle-arrow {          font-size: 12px;          transition: transform 0.2s ease;        }        .hb-toggle-image.expanded .hb-toggle-arrow {          transform: rotate(90deg);        }        .hb-image-preview {          background: #f5f5f5;          border-radius: 4px;          text-align: center;          padding: 8px;          transition: all 0.3s ease;        }        .hb-image-preview img {          max-width: 100%;          max-height: 300px;          object-fit: contain;        }        .hb-modal-footer {          padding: 16px;          border-top: 1px solid #eee;          display: flex;          justify-content: flex-end;          gap: 8px;        }        .hb-button {          padding: 8px 16px;          border-radius: 4px;          border: none;          font-weight: 500;          cursor: pointer;          font-size: 14px;          line-height: 1.5;          font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;          text-decoration: none;          text-transform: none;          display: inline-flex;          align-items: center;          justify-content: center;          transition: all 0.2s ease;        }        .hb-button:disabled {          opacity: 0.5;          cursor: not-allowed;        }        .hb-button-primary {          background: #2196f3;          color: white;        }        .hb-button-primary:not(:disabled):hover {          background: #1976d2;        }        .hb-button-secondary {          background: #f5f5f5;          background-color: #f5f5f5;          color: #333;        }        .hb-button-secondary:hover {          background: #e5e5e5;        }        .hb-success-message {          text-align: center;          padding: 32px 16px;        }        .hb-success-message h3 {          color: #4caf50;          margin: 0 0 8px;        }      `,l.innerHTML=c,l.prepend(u),document.body.appendChild(h);let f=l.querySelector(".hb-report-modal"),g=l.querySelector(".hb-close-button"),q=l.querySelector(".hb-cancel-button"),y=l.querySelector(".hb-submit-button"),Q=l.querySelectorAll('input[name="report-type"]'),Y=l.querySelector(".hb-modal-content"),M=l.querySelector(".hb-toggle-image"),J=l.querySelector(".hb-image-preview"),X=l.querySelector(".hb-toggle-text"),x=()=>h.remove();M.addEventListener("click",()=>{let b=M.classList.contains("expanded");M.classList.toggle("expanded"),J.style.display=b?"none":"block",X.textContent=b?"Show Image":"Hide Image"}),g.addEventListener("click",x),q.addEventListener("click",x),f.addEventListener("click",b=>{b.target===f&&x()}),Q.forEach(b=>{b.addEventListener("change",()=>{y.disabled=!1})}),y.addEventListener("click",async()=>{let b=l.querySelector('input[name="report-type"]:checked').value,D=l.querySelector(".hb-image-preview img").src;y.disabled=!0,y.textContent="Sending...";let K="625186413",Z="1528363305",ee="2049916266",te="2050137681",re=await chrome.storage.sync.get("hb-settings"),se="928202137",oe=a?.originalSrc,ae="https://docs.google.com/forms/u/0/d/e/1FAIpQLSfHbTsJaydX__sCbv7aGf0RiVBxTcbBSzv5LU-VALJm1TFnlg/formResponse",ne=new URLSearchParams({[`entry.${K}`]:b,[`entry.${Z}`]:D,[`entry.${se}`]:oe,[`entry.${ee}`]:o,[`entry.${te}`]:JSON.stringify(re["hb-settings"])});try{b==="false-negative"?chrome.runtime.sendMessage({type:"relay-false-negative",tabId:n,imageUrl:D}):b==="false-positive"&&chrome.runtime.sendMessage({type:"relay-false-positive",tabId:n,imageUrl:D}),await fetch(ae,{method:"POST",mode:"no-cors",body:ne}),Y.innerHTML=`            <div class="hb-success-message">              <h3>Thank You!</h3>              <p>Your report has been submitted successfully.<br>We appreciate your help in improving HaramBlur.</p>            </div>          `,setTimeout(x,2e3)}catch(ie){console.error("Failed to send report:",ie),y.textContent="Send Report",y.disabled=!1}})},args:[e.srcUrl,e.pageUrl,t,s.id]})}
External destination used by the report flow
    • docs.google.com

    Receives the Google Forms formResponse POST containing report fields and the saved settings JSON.

Startup fetch applies GitHub blocklist rules

At init, the background code fetches a blocklist JSON from raw.githubusercontent.com with caching off, then converts blocked/allowed arrays into declarativeNetRequest rules, including redirects to the blocked page, with no signature check.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-829
Source
Dynamic sandbox
What actually happens
You did this

The extension initializes its background rule set.

The install and update listener calls the rule-update function after other startup work completes.

The extension did this

The service worker fetches a GitHub JSON blocklist and applies it as browser redirect and allow rules.

Captured request
GEThttps://raw.githubusercontent.com/alganzoryP/hb-related/refs/heads/main/blocklist.json
Remote JSON fields converted into browser rules
  • Blocked URL patterns
    {"blocked":["exampleadult","sampleblocked"]} (illustrative)

    Controls which page addresses are redirected to the extension blocked page.

  • Allowed domains
    {"allowed":["example","trustedsite"]} (illustrative)

    Controls which page addresses bypass the redirect rules.

  • Redirect destination
    chrome-extension://pbcoegikffnadpahojjhgdladmmddeji/src/assets/blocked/blocked.html?url=https://example.com/page (illustrative)

    Shows where matching pages are sent instead of loading normally.

  • User allowed sites
    ["work.example.com"] (illustrative)

    Your locally allowed sites are added as higher-priority allow rules.

The code that does this

Remote blocklist fetch and dynamic rule replacement

Readable version

Rule fetch and update functions

dist/background.js
var pe=100,H=1e4,be=async()=>{try{let e=await(await fetch("https://raw.githubusercontent.com/alganzoryP/hb-related/refs/heads/main/blocklist.json",{cache:"no-cache"})).json(),{blocked:t,allowed:r}=e,o=[],a=t.reduce((u,f,g)=>g%5===0?(o.push(u),f):`${u}|${f}`);o.push(a);let n=o.map((u,f)=>({id:pe+f,priority:1,action:{type:"redirect",redirect:{regexSubstitution:`${chrome.runtime.getURL("src/assets/blocked/blocked.html")}?url=\\0`}},condition:{regexFilter:`^[^:]*:\\/\\/(?:[^/]*(?:${u}))[^/]*\\/`,resourceTypes:["main_frame"]}})),i=r.map((u,f)=>({id:H+f,priority:2,action:{type:"allow"},condition:{regexFilter:`^[^:]*:\\/\\/(?:(?:www\\.)?(?:[^\\/]*\\.)?${u}\\.[^\\/]*)\\/`,resourceTypes:["main_frame"]}})),l=((await chrome.storage.sync.get("hb-allowed-sites"))["hb-allowed-sites"]||[]).map((u,f)=>({id:H+r.length+f,priority:3,action:{type:"allow"},condition:{urlFilter:`||${u}^`,resourceTypes:["main_frame"]}}));return[...n,...i,...l]}catch(s){return console.error("Error creating rules:",s),[]}},P=async()=>{try{let e=(await chrome.declarativeNetRequest.getDynamicRules()).map(r=>r.id),t=await be();await chrome.declarativeNetRequest.updateDynamicRules({removeRuleIds:e,addRules:t})}catch(s){console.error("Error initializing rules:",s)}}

Install/update path that invokes rule update

dist/background.js
chrome.runtime.onInstalled.addListener(async s=>{try{await R(),await S(),await he();let e=chrome.runtime.getManifest();if(s?.reason==="install")console.log("New installation detected - showing onboarding wizard"),chrome.tabs.create({url:chrome.runtime.getURL("dist/options/options.html?tour=full")});else if(s?.reason==="update"){if(s.previousVersion==e.version)return;console.log(`Extension updated from ${s.previousVersion} to ${e.version}`),chrome.tabs.create({url:chrome.runtime.getURL("dist/options/options.html?tour=update")})}P()}catch(e){console.error("Error during extension initialization:",e)}});
External host supplying navigation rules
    • raw.githubusercontent.com

    Serves the blocklist JSON whose blocked and allowed arrays are applied as main-frame request rules.

Updated 30 September 2026pbcoegikffnadpahojjhgdladmmddeji