Is uTab - Unlimited Custom Dashboard safe?

Medium risk

uTab routes every new-tab search through search.utab.io with a persistent per-user id before redirecting to Bing.

Each query typed into the new-tab search box is sent to search.utab.io tagged with a persistent per-user UUID and the extension's runtime id, then forwarded on to Bing. The service worker also fetches a remote config from utab.io/api/ every 15 minutes, and that config can override where searches are sent. Separately, the URLs of your bookmarks and pages saved via the 'Save to uTab' menu are sent to the third-party screenshot service sshot.org to generate thumbnail previews.

uTab.iov3.6Chrome Web Store
51Risk

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

Publishers can request a review.

Findings

Bookmark URLs sent to sshot.org previews

MediumunwantedCWE-200Dynamic sandbox

Code analysis shows that saving a page or link to uTab, or uTab preparing bookmark-folder previews, sends the saved URL to https://sshot.org in the url parameter, with a Basic authorization header using a bundled credential, redacted below.

What actually happens

You did this

You save a page or link to uTab, or ask uTab to prepare previews for a bookmark folder.

The extension did this

The extension sends the saved page URL to sshot.org so that service can generate a thumbnail preview.

Captured request

GEThttps://sshot.org?url=https%3A%2F%2Fexample.com%2Fprivate%2Froadmap%3Fticket%3DINC-4821&width=200&height=112&zoom=0.15&clipRect=0%2C0%2C200%2C112

The fetch call expects image bytes and converts the response array buffer to a data:image/png;base64 preview.

Headers
Authorization
Basic <redacted>

Fields sent to the screenshot service

  • Saved page URL
    https://example.com/private/roadmap?ticket=INC-4821

    The exact page you saved is sent to the screenshot service, including any path and query string in that address.

  • Preview dimensions
    width=200&height=112&zoom=0.15

    These values tell the service what size thumbnail to render for the saved page.

  • Bundled service credential
    Basic <redacted>

    The extension includes a credential for the screenshot service in its shipped code.

How user actions map to preview requests

  1. same action

    You did

    You choose Save to uTab for the current page or link.

    The extension did

    The extension stores that URL and immediately requests a preview from sshot.org.

  2. per matching bookmark

    You did

    You add or edit a folder that contains saved bookmarks.

    The extension did

    The extension requests previews for bookmark entries whose preview is still marked as loading.

The code that does this

Preview generation sends bookmark URLs to sshot.org

Readable version

Folder edit/add triggers preview refresh

deobfuscated/js/background.js
FOLDER_EDIT: function(t, e) {    t.commit("FOLDER_EDIT", e), chrome.runtime.sendMessage({        action: "getFolderPreviews",        folder: e    }), chrome.runtime.sendMessage({        action: "update_context_menu"    });},FOLDER_ADD: function(t, e) {    e.bookmarks.length > 0 && (e.empty = !1), t.commit("FOLDER_ADD", e), chrome.runtime.sendMessage({        action: "getFolderPreviews",        folder: e    }, (function() {        chrome.runtime.lastError;    })), chrome.runtime.sendMessage({        action: "update_context_menu"    }, (function() {        chrome.runtime.lastError;    })), 1 == t.state.storage.settings.folders.length && t.commit("FOLDER_SELECT", e);}

Save to uTab stores the page URL and requests a preview

deobfuscated/js/background.js
{    key: "createContextMenu",    value: function() {        var t = this;        chrome.contextMenus.removeAll((function() {            if (!a.a.getters.isNoFolders) {                var e = null;                a.a.state.storage.settings.folders && a.a.state.storage.settings.folders.length > 1 && (e = "parentDropDown",                 chrome.contextMenus.create({                    title: "Save to uTab",                    id: e,                    contexts: [ "page", "link" ]                }, (function() {                    chrome.runtime.lastError;                })));                for (var n = 0; n < a.a.state.storage.settings.folders.length; n++) {                    var r = a.a.state.storage.settings.folders[n], o = 'Save to "' + r.name + '"';                    1 == a.a.state.storage.settings.folders.length && (o = "Save to uTab"), chrome.contextMenus.create({                        title: o,                        id: r.id,                        parentId: e,                        contexts: [ "page", "link" ]                    }, (function() {                        chrome.runtime.lastError;                    }));                }                chrome.contextMenus.onClicked.addListener((function(e, n) {                    var r = a.a.state.storage.settings.folders.find((function(t) {                        return t.id === e.menuItemId;                    }));                    if (r) {                        var o = {                            id: Object(u.b)(),                            _id: Object(u.b)(),                            title: n.title,                            url: n.url,                            icon: n.favIconUrl || "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB3UlEQVQ4T6WTP2hTURjFz7nP2EpxK0pBEFwcnDu3ZMiQWSKK0CQmmgSHVgdBUPEPoqBYHJLQvuZPEZSAq2CGqoOLLi4ijrqIXcVom+Qeua95+VONS+/0OJfvd893vu8RezzcXX92cXEm8nPrqYRZSQfcPclfJN63pyZOP1le/jZcMwJInS8sqWvvC4iQsBA2BbYBHQk4QJueuVJdKT4KIX2AK7Zd+xCAJXEzHoveTSQS3WTm4rzUeTX0qoxnLoeQAOBsez+2vgKiJ56oVEqfnZ7KFB5Y2DMQZnbZbncOTh517QSAVDb/2lrNkbhe88u3nZbMFmZl7btxGRvDN9XV0nwAWEjnWjSYiMei+53tHnROVsUe4JiAyREXZKvml6Z2AOdylsD32lp5xGpYkMzkn0k6NQBoG2CkvlY2fYBLeeiFLsEvoNbjseidZvPtdEfbSyCmIbwE9FjA4QEgnWuBCGa++3g0CxW/uB7qjUbDe9HccA5+91sIQ/xnYORK3S9dGLSTuybh1kiIQ2Pc14eQPoAPBvpU9csbTk+n88e71EcCdmSMQeqDRdrJwuBkfbX83H32bF+VcMPd/LVIfXvZ/CVY3XOrDKJDYBOCAXFIgkt8/CqHkD39TOO27n/6HyyR4xF0kx9xAAAAAElFTkSuQmCC",                            preview: "load"                        };                        a.a.commit("BOOKMARK_MANAGER_ADD", {                            item: o,                            folder: r                        }), a.a.dispatch("FOLDER_BOOKMARK_ADD", {                            item: o,                            folder: r                        }), t.getPreview(o.url).then((function(t) {                            o.preview = t, a.a.commit("FOLDER_BOOKMARK_PREVIEW_SET", {                                folder: r,                                bookmark: o                            });                        }));                    }                }));            }        }));    }}

getPreview fetches sshot.org with the saved URL

deobfuscated/js/background.js
{    key: "getPreview",    value: function(e) {        return new Promise((function(n, r) {            var o = new Headers;            o.set("Authorization", "Basic " + btoa("user:<redacted>")), fetch("https://sshot.org?url=" + encodeURIComponent(e) + "&width=200&height=112&zoom=0.15&clipRect=0%2C0%2C200%2C112", {                method: "get",                headers: o,                responseType: "arraybuffer"            }).then((function(t) {                return t.arrayBuffer();            })).then((function(e) {                n("data:image/png;base64," + t.from(e, "binary").toString("base64"));            })).catch((function(t) {                n(null);            })).finally((function() {                n(null);            }));        }));    }}

getFolderPreviews calls getPreview for each loading bookmark

deobfuscated/js/background.js
{    key: "getFolderPreviews",    value: function(t) {        var e = this, n = a.a.state.storage.settings.folders.find((function(e) {            return e.id == t.id;        }));        n.bookmarks.forEach((function(t, r) {            "gap" != t.type && "load" == t.preview && e.getPreview(t.url).then((function(e) {                t.preview = e, a.a.commit("FOLDER_BOOKMARK_PREVIEW_SET", {                    folder: n,                    bookmark: t                });            }));        }));    }}

External host that receives saved URLs

    • sshot.org

    Receives each saved page or bookmark URL and returns a thumbnail image for uTab previews.

Remote config can change new-tab searches

MediumunwantedCWE-829Dynamic sandbox

We observed the extension request https://utab.io/api/ during dynamic analysis.

What actually happens

You did this

You search from the uTab new-tab page after the extension has refreshed its configuration.

The extension did this

The extension can build the destination URL from a template returned by utab.io instead of its built-in search URL.

Captured request

GEThttps://utab.io/api/

200 OK observed during dynamic analysis; no request body was recorded for this config fetch.

Fields that determine the search redirect

  • Search query
    weather tomorrow

    Your typed search terms are inserted into the selected destination URL.

  • Server-supplied template
    https://search.utab.io/search/?q={KEYWORD}

    A value from the remote configuration can decide which site receives your search.

  • Extension user ID
    f4b1d629-8d7a-4d6f-9f2c-58b0c6d7e0a2

    The built-in search URL includes an extension-generated identifier alongside the query.

When this fires

Every 15 minutes

The extension refreshes the remote configuration on startup and then schedules another refresh roughly every 15 minutes.

The code that does this

Config refresh and search URL replacement in the service worker

Readable version

Constructor schedules the config refresh

deobfuscated/js/background.js
new (i()((function t() {    var e = this;    r()(this, t), this.config = null, this.bookmarkPreviewTabId = null, this.bookmarkPreviewTabOpenerId = null,     this.searchCount = 0, this.fetchSlidersDate = null, this.updateSliderDate = Date.now(),     chrome.runtime.setUninstallURL("https://utab.io/uninstall.html", (function() {})),     Object(a.b)().then((function() {        e.onStorageLoaded();    })), chrome.runtime.onInstalled.addListener((function(t) {        "update" == t.reason && setTimeout((function() {            null == a.a.state.storage.settings.background && a.a.dispatch("SET_BACKGROUND_APP", {                type: "default",                index: 3            });        }), 1e3), fetch("https://utab.io/beg/?y=" + t.reason).then((function(t) {            return t.json();        })).then((function(n) {            e.newFeaturesReply(t.reason, n);        }));    })), this.initListeners(), this.getConfig(), setInterval((function() {        e.getConfig();    }), 9e5);})

getConfig fetches JSON from utab.io/api/

deobfuscated/js/background.js
{    key: "getConfig",    value: function() {        var t = this;        Date.now() - a.a.state.storage.user.fetchConfigDate > 9e5 && fetch("https://utab.io/api/").then((function(t) {            return t.json();        })).then((function(e) {            a.a.commit("SET_FETCH_CONFIG_DATE", Date.now()), t.config = e;        })).catch((function(t) {}));    }}

Search handler applies config.searchTemplate

deobfuscated/js/background.js
case "search":    var o = "https://search.utab.io/search/?id=".concat(chrome.runtime.id, "&source=newtab&q=").concat(encodeURIComponent(e.query), "&uid=").concat(a.a.state.storage.user.id, "&se=") + "bing";    t.config && t.config.searchTemplate && (o = t.config.searchTemplate.replace("{KEYWORD}", e.query)),     chrome.tabs.update(null, {        url: o    });    break;

External hosts involved in this behavior

    • utab.io

    Provides the remote configuration fetched by the extension.

    • search.utab.io

    Receives searches when the built-in search URL remains in use.

What it can do

Permissions this extension asks for, as declared in version 3.6. 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

    <all_urls>

  • Read and change your bookmarks

    bookmarks

  • Store data in your browser

    storage

  • Store an unlimited amount of data in your browser

    unlimitedStorage

  • Act on the current tab, but only after you click the extension

    activeTab

  • Run searches with your default search engine

    search

  • Run its own code inside the pages you visit

    scripting

  • Add items to the right-click menu

    contextMenus

favicon

Where it sends data

Destinations our analysis observed uTab - Unlimited Custom Dashboard 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.

  • search.utab.io

    uTab - Unlimited Custom Dashboard sends data to search.utab.io. One other extension we have analyzed sends data here.

  • utab.io

    uTab - Unlimited Custom Dashboard sends data to utab.io. One other extension we have analyzed sends data here.

  • sshot.org

    uTab - Unlimited Custom Dashboard sends data to sshot.org. No other extension we have analyzed sends data here.

Updated 30 September 2026npmjjkphdlmbeidbdbfefgedondknlaf