Is Shinigami Eyes safe?
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.
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
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.
The code places the container's outerHTML into a snippet field before upload.
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 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.
| Field | Value | Why it matters | |
|---|---|---|---|
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 content script selects a surrounding container and stores its outerHTML
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;
}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 background script queues the snippet and submits it to shini-api.xyz
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);
}
}
}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();
}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);
});
});Before upload, the extension stores the label submission locally with the page context and the surrounding HTML it selected.
browser.storage.local overrides pending-submissions queue{
"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"
}- 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.