Is Shinigami Eyes safe?

Medium risk

Shinigami Eyes collects surrounding post DOM from social media pages and transmits it to shini-api.xyz when a user labels an account.

When a user applies a label (mark) to an account on Twitter, Reddit, Mastodon, or similar sites, the extension captures the outerHTML of the enclosing post container—which can include content from unrelated users—and sends it to shini-api.xyz along with a persistent installation ID, the target identifier, and the page URL. The extension also periodically fetches remote configuration from GitHub and loads bloom filter binaries from URLs specified in that configuration, with no cryptographic integrity check on the downloaded binary data.

Shinigami Eyesv1.0.37Firefox Add-ons
47Risk
Who publishes it

Shinigami Eyes - no other listings under this identity, 5 shared hostnames

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

Publisher
Shinigami Eyes

Shared hosts - 5 hostnames

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.

rationalwiki.org
Also called by 1 other listing: Shinigami Eyes
shini-api.xyz
Also called by 1 other listing: Shinigami Eyes
shinigami-eyes.github.io
Also called by 1 other listing: Shinigami Eyes
home.comcast.net
Also called by 4 other listings, including Shinigami Eyes, spyhunter%C2%AE-web-security, SpyHunter® Web Security
willwhim.wpengine.com
Also called by 4 other listings, including spyhunter%C2%AE-web-security, SpyHunter® Web Security

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

Publishers can request a review.

Findings

Shinigami Eyes Sends Post HTML With Account Labels

MediumunwantedCWE-359Dynamic sandbox

Labeling a social media account makes Shinigami Eyes read the surrounding post or thread container and include its HTML in the vote submission to shini-api.xyz.

What actually happens

You did this

You label a social media account from the right-click menu.

This happens after the extension has been accepted and the selected link can be resolved to an account identifier.

The extension did this

The extension reads the surrounding post or thread HTML and submits it with the label.

The same request also carries the current page URL, installation ID, label choice, extension version, and bloom-filter version.

Captured request

POSThttps://shini-api.xyz/submit-vote

The code expects a 200 response whose text body is SUCCESS.

Fields prepared for the vote submission

  • Surrounding post HTML
    Illustrative Reddit container: <div class="Comment"><a href="/user/alex">u/alex</a><p>comment text</p></div>

    This can include the post, comment, or thread container around the account you label, including content from other people on the page.

  • Current page URL
    https://www.reddit.com/r/all/comments/1b2c3d/thread_title/

    This shows which social media page or thread was open when you submitted the label.

  • Installation ID
    7f6f6a0e-41a4-4c77-9b32-15eec3f11190

    This lets repeated submissions from the same browser install be linked together.

  • Account label
    t-friendly

    This records the label you chose for the selected account.

  • Extension data versions
    version 165, bloomVersion 2024-10-01

    This identifies which extension and bloom-filter versions were active when the label was submitted.

The code that does this

The content script selects a surrounding container and stores its outerHTML

Readable version

Container selection by supported site

deobfuscated/content.js
function getSnippet(node) {    try {        return getSnippetImpl(node);    }    catch (e) {        console.warn("Could not obtain snippet: " + e);        return null;    }}function getSnippetImpl(node) {    var _a, _b, _c;    if (hostname == 'facebook.com') {        const pathname = window.location.pathname;        const isPhotoPage = pathname.startsWith('/photo') || pathname.includes('/photos/') || pathname.startsWith('/video') || pathname.includes('/videos/');        if (isPhotoPage) {            const sidebar = document.querySelector('[role=complementary]');            if (sidebar)                return sidebar.parentElement;        }        const isSearchPage = pathname.startsWith('/search/');        return getMatchingAncestor(node, x => {            if (x.getAttribute('role') == 'article' && (isSearchPage || x.getAttribute('aria-labelledby')))                return true;            var dataset = x.dataset;            if (!dataset)                return false;            if (dataset.ftr)                return true;            if (dataset.highlightTokens)                return true;            if (dataset.gt && dataset.vistracking)                return true;            return false;        });    }    if (hostname == 'reddit.com')        return getMatchingAncestorByCss(node, '.scrollerItem, .thing, .Comment');    if (hostname == 'twitter.com')        return getMatchingAncestorByCss(node, '.stream-item, .permalink-tweet-container, article');    if (hostname == 'disqus.com')        return getMatchingAncestorByCss(node, '.post-content');    if (hostname == 'medium.com')        return getMatchingAncestorByCss(node, '.streamItem, .streamItemConversationItem');    if (hostname == 'youtube.com')        return getMatchingAncestorByCss(node, 'ytd-comment-renderer, ytd-video-secondary-info-renderer');    if (hostname == 'tumblr.com')        return getMatchingAncestor(node, x => (x.dataset && !!(x.dataset.postId || x.dataset.id)) || x.classList.contains('post'));    if (hostname == 'threads.net') {        if (location.pathname.includes('/post/')) {            return getOutermostMatchingAncestor(node, x => getAbsoluteOffsetTop(x) > 30);        }        else {            return getOutermostMatchingAncestor(node, x => x.dataset.pressableContainer == 'true');        }    }    if (hostname == 'bsky.app') {        if (location.pathname.includes('/post/')) {            return (_b = (_a = getOutermostMatchingAncestor(node, x => { var _a; return (_a = x.dataset.testid) === null || _a === void 0 ? void 0 : _a.startsWith('postThreadItem-by-'); })) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.parentElement;        }        else {            return getOutermostMatchingAncestor(node, x => { var _a; return (_a = x.dataset.testid) === null || _a === void 0 ? void 0 : _a.startsWith('feedItem-by-'); });        }    }    if (isMastodon)        return (_c = (/\/\d+$/.test(location.pathname) ? getMatchingAncestorByCss(node, '.scrollable') : null)) !== null && _c !== void 0 ? _c : getMatchingAncestorByCss(node, '.status, article, .detailed-status__wrapper, .status__wrapper-reply');    return null;}

Message handler adds snippet.outerHTML to the vote message

deobfuscated/content.js
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {    if (message.updateAllLabels || message.confirmSetLabel) {        displayConfirmation(message.confirmSetIdentifier, message.confirmSetLabel, message.badIdentifierReason, message.confirmSetUrl, null);        updateAllLabels(true);        return undefined;    }    message.contextPage = window.location.href;    const originalTarget = lastRightClickedElement;    let target = originalTarget; // message.elementId ? browser.menus.getTargetElement(message.elementId) : null;    while (target) {        if (target instanceof HTMLAnchorElement)            break;        target = target.parentElement;    }    if (target && target.href != message.url)        target = null;    var identifier = target ? getIdentifier(target, originalTarget) : getIdentifier(message.url);    if (!identifier) {        displayConfirmation(null, 'bad-identifier', null, message.url, target);        return undefined;    }    (async () => {        var _a, _b;        message.identifier = identifier;        if (identifier.startsWith('facebook.com/'))            message.secondaryIdentifier = getIdentifier(message.url);        var snippet = getSnippet(target);        message.linkId = ++lastGeneratedLinkId;        if (target)            target.setAttribute('shinigami-eyes-link-id', '' + lastGeneratedLinkId);        if (hostname == 'twitter.com') {            try {                const twitterUserName = (_a = captureRegex(identifier, /^twitter\.com\/(.*)$/)) === null || _a === void 0 ? void 0 : _a.toLowerCase();                if (twitterUserName) {                    const request = {                        linkId: message.linkId,                        wantIdForScreenName: twitterUserName                    };                    const response = await findTwitterNumericIdsFirefox(request);                    const twitterMapping = (_b = response.mappings) === null || _b === void 0 ? void 0 : _b.filter(x => { var _a; return twitterUserName == ((_a = x.userName) === null || _a === void 0 ? void 0 : _a.toLowerCase()); })[0];                    if (twitterMapping)                        message.secondaryIdentifier = 'twitter.com/i/user/' + twitterMapping.numericId;                }            }            catch (error) {                console.warn(error);            }        }        message.snippet = snippet ? snippet.outerHTML : null;        var debugClass = 'shinigami-eyes-debug-snippet-highlight';        if (snippet && message.debug) {            snippet.classList.add(debugClass);            if (message.debug <= 1)                setTimeout(() => snippet.classList.remove(debugClass), 1500);        }        sendResponse(message);    })();    return true;});

The code that does this

The background script queues the snippet and submits it to shini-api.xyz

Readable version

Submission body construction and POST

deobfuscated/background.js
async function submitPendingRatings() {    const submitted = getPendingSubmissions().filter(x => !submissionsBeingSubmitted.has(x));    for (const entry of submitted) {        submissionsBeingSubmitted.add(entry);    }    let plainRequest = {        installationId: installationId,        lastError: lastSubmissionError,        entries: submitted    };    console.log('Submitting request:');    console.log(plainRequest);    let actualRequest = plainRequest;    if (!disableAsymmetricEncryption) {        try {            actualRequest = await encryptSubmission(plainRequest);        }        catch (e) {            // If something goes wrong, fall back to the old behavior (of course, we still have HTTPS).            // While the above encryption process has been tested on both Chromium- and Gecko-based browsers,            // the real world behavior might be different.            // If no significant issues appear, this catch clause will be removed in a subsequent version of Shinigami Eyes.            actualRequest.encryptionError = e + '';        }    }    lastSubmissionError = null;    try {        const controller = new AbortController();        setTimeout(() => controller.abort(), 90000);        const response = await fetch('https://shini-api.xyz/submit-vote', {            body: JSON.stringify(actualRequest),            method: 'POST',            credentials: 'omit',            signal: controller.signal        });        if (response.status != 200)            throw ('HTTP status: ' + response.status);        const result = await response.text();        if (result != 'SUCCESS')            throw 'Bad response: ' + ('' + result).substring(0, 20);        overrides[PENDING_SUBMISSIONS] = getPendingSubmissions().filter(x => submitted.indexOf(x) == -1);        browser.storage.local.set({ overrides: overrides });    }    catch (e) {        lastSubmissionError = '' + e;    }    finally {        for (const entry of submitted) {            submissionsBeingSubmitted.delete(entry);        }    }}

Snippet trimming and queueing

deobfuscated/background.js
function saveLabel(response) {    if (accepted) {        if (!getPendingSubmissions()) {            overrides[PENDING_SUBMISSIONS] = Object.getOwnPropertyNames(overrides)                .map(x => { return { identifier: x, label: overrides[x] }; });        }        overrides[response.identifier] = response.mark;        if (response.secondaryIdentifier && !response.secondaryIdentifier.startsWith('twitter.com/i/user/'))            overrides[response.secondaryIdentifier] = response.mark;        browser.storage.local.set({ overrides: overrides });        response.version = CURRENT_VERSION;        response.bloomVersion = bloomFilters.bloomVersion;        response.submissionId = (Math.random() + '').replace('.', '');        let totalSize = 0;        for (const entry of getPendingSubmissions()) {            if (entry.snippet)                totalSize += entry.snippet.length;        }        if (totalSize > 2000000) {            for (const entry of getPendingSubmissions()) {                entry.snippet = null;                entry.trimmed = true;            }        }        if (response.snippet && response.snippet.length > 10000000) {            response.snippet = null;            response.trimmed = true;        }        getPendingSubmissions().push(response);        submitPendingRatings();        //console.log(response);        sendMessageToContent(response.tabId, response.frameId, {            updateAllLabels: true,            confirmSetIdentifier: response.identifier,            confirmSetUrl: response.url,            confirmSetLabel: response.mark || 'none'        });        //browser.tabs.executeScript(response.tabId, {code: 'updateAllLabels()'});        return;    }    uncommittedResponse = response;    openHelp();}

Context-menu label action sent to the content script

deobfuscated/background.js
browser.contextMenus.onClicked.addListener(function (info, tab) {    if (info.menuItemId == 'help') {        openHelp();        return;    }    if (info.menuItemId == 'options') {        openOptions();        return;    }    const tabId = tab.id;    const frameId = info.frameId;    var label = info.menuItemId.substring('mark-'.length);    if (label == 'none')        label = '';    browser.tabs.sendMessage(tabId, {        mark: label,        url: info.linkUrl,        tabId: tabId,        frameId: frameId,        // elementId: info.targetElementId,        debug: overrides.debug    }, { frameId: frameId }, response => {        if (!response || !response.identifier) {            return;        }        if (response.mark) {            if (badIdentifiers[response.identifier]) {                sendMessageToContent(tabId, frameId, {                    confirmSetIdentifier: response.identifier,                    confirmSetUrl: response.url,                    confirmSetLabel: 'bad-identifier',                    badIdentifierReason: badIdentifiersReasons[response.identifier]                });                return;            }            if (response.secondaryIdentifier && badIdentifiers[response.secondaryIdentifier])                response.secondaryIdentifier = null;        }        response.tabId = tabId;        response.frameId = frameId;        saveLabel(response);    });});

What's stored on your device

Before upload, the extension stores the label submission locally with the page context and the surrounding HTML it selected.

Location
browser.storage.local overrides pending-submissions queue
Contents
{  "mark": "t-friendly",  "snippet": "Illustrative value based on the selected Reddit container: <div class=\"Comment\"><a href=\"/user/alex\">u/alex</a><p>comment text</p></div>",  "version": "CURRENT_VERSION",  "identifier": "reddit.com/user/alex",  "contextPage": "https://www.reddit.com/r/all/comments/1b2c3d/thread_title/",  "bloomVersion": "bloomFilters.bloomVersion",  "submissionId": "06519771462291288"}

External service receiving the vote submission

    • shini-api.xyz

    Receives POST /submit-vote submissions containing queued account labels, installation ID, page URL, and any snippet field kept by the trimming checks.

What it can do

Permissions this extension asks for, as declared in version 1.0.37. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on every site you visit

    *://*/*

  • Add items to the right-click menu

    contextMenus

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed Shinigami Eyes contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • raw.githubusercontent.comwidely used

    Shinigami Eyes sends data to raw.githubusercontent.com. A widely used service: 32 other extensions we have analyzed send data here.

  • shini-api.xyz

    Shinigami Eyes sends data to shini-api.xyz. One other extension we have analyzed sends data here.

Updated 2 November 2026amo-998734