Is Site Palette safe?

High risk

Site Palette is high risk. Creating a palette captures the page as a stitched JPEG and posts it to api.palette.site/palettes with the page URL, a hostname title, color data, and your account Bearer token. A passive session didn't exercise this; no live body captured.…

Sliday LTDv2.25Chrome Web Store
75Risk
Who publishes it

Sliday LTD - no other listings under this identity, 4 shared hostnames

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

Publisher
Sliday LTD
Registered address
12C Cranbrook Place, Auckland 1071, NZ
Registered contact
Sliday Limited

Shared hosts - 4 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.

api.palette.site
Also called by 1 other listing: Site Palette
karmabot.chat
Also called by 2 other listings, including Site Palette
palette.site
Also called by 3 other listings, including Site Palette
artsexperiments.withgoogle.com
Also called by 5 other listings, including Site Palette, ProTab | Arbfonts

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

Publishers can request a review.

Findings

Full-page screenshots uploaded when creating palettes

Creating a palette captures the page as a stitched JPEG and posts it to api.palette.site/palettes with the page URL, a hostname title, color data, and your account Bearer token.

A passive session didn't exercise this; no live body captured.

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

You create a color palette from the page you are viewing.

The create button starts page measurement, color extraction, and screenshot capture for the active tab.

The extension did this

The extension packages the page URL, a full-page JPEG screenshot, and color data for upload.

The upload path uses the stored Site Palette account token and posts to api.palette.site.

Fields the source code places in the palette upload
  • Page URL
    https://intranet.example.com/projects/q3-roadmap?view=board (illustrative)

    Shows which page you used to create the palette, including path and query details present in the address bar.

  • Page title
    intranet.example.com (illustrative)

    Adds context about the captured page by deriving a title from the page hostname.

  • Full-page image
    data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD (illustrative)

    Can include visible text, images, account names, dashboards, and other information shown on the page when the palette is created.

  • Generated colors
    {"hex":"#1f6feb","name":"Dodger Blue"} (illustrative)

    Records the colors extracted from the page and links the palette result to the captured page.

  • Site Palette token
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 (illustrative)

    Associates the upload with the signed-in Site Palette account used by the extension.

Captured request
POSThttps://api.palette.site/palettes

No live response was captured; passive dynamic analysis did not exercise the login and create-palette flow.

Headers
Content-Type
application/json
Authorization
Bearer value from chrome.storage.local['palette-token']
The code that does this

The shipped and readable code paths behind the upload

Readable version

Popup create button starts capture for the active tab

popup.js
function v(t) {  chrome.runtime.sendMessage({    command: "get-tab",    tabId: t.id,    tabUrl: t.url  }), chrome.tabs.sendMessage(t.id, {    msg: "getPageDetails"  }, (function(t) {    g.classList.contains("container_hide") || g.classList.add("container_hide"), f.classList.contains("container_hide") && f.classList.remove("container_hide")  }))}a.addEventListener("click", (async function(t) {  const e = await L;  chrome.scripting.executeScript({    target: {      tabId: e.id    },    func: i.collectVisibleColors,    args: [{      maxColors: 10,      textWeight: .3,      samples: 1600    }]  }, (t => {    if (t && t[0]) {      const e = t[0].result;      chrome.runtime.sendMessage({        command: "viewport-colors-analyzed",        data: {          palette: e.palette        }      })    }    chrome.tabs.sendMessage(e.id, {      command: "are_you_there_content_script?"    }, (function(t) {      "yes" !== (t = t || {}).status ? chrome.scripting.executeScript({        target: {          tabId: e.id        },        files: ["scroller.js", "getPalette.js", "sitePalette.js"]      }, (() => {        v(e)      })) : "yes" === t.status && v(e), chrome.runtime.lastError && console.log("err", chrome.runtime.lastError)    }))  }))}))

Service worker captures viewport JPEGs

background.js
capturePage: function(e, t) {  var o = this;  setTimeout((function() {    chrome.tabs.captureVisibleTab(null, {      format: "jpeg"    }, (function(s) {      if (void 0 !== s)        if (t) {          o.resetPage();          var a, r = o.getNextViewTabUrl();          chrome.tabs.onUpdated.addListener((function t(n, i) {            n == a && "complete" == i.status && (chrome.tabs.onUpdated.removeListener(t), chrome.tabs.sendMessage(o.tabId, {              msg: "setScreenshotUrl",              viewTabUrl: r,              dataURI: s,              tabUrl: o.tabURL,              position: e,              size: o.size,              colors: o.cssColors            }))          })), chrome.tabs.create({            url: r          }, (function(e) {            a = e.id          }))        } else chrome.tabs.sendMessage(o.tabId, {          msg: "getCapturePage",          dataURI: s,          position: e,          size: o.size        }), o.scrollTo(e + o.scrollBy);      else console.log("Error", s), chrome.tabs.sendMessage(o.tabId, {        msg: "showError",        error: "Error",        originalParams: o.originalParams      })    }))  }), 550)}

Canvas code emits the final JPEG data URL

getPalette.js
(() => {  let e = document.createElement("canvas"),    a = e.getContext("2d");  chrome.runtime.onMessage.addListener(((i, t, o) => {    if ("setScreenshotUrl" === i.msg)(n = new Image).onload = function() {      a.drawImage(n, 0, i.position * window.devicePixelRatio), chrome.runtime.sendMessage({        msg: "screenshotCanvasReady",        canvasURL: e.toDataURL("image/jpeg", .4),        tabUrl: i.tabUrl,        viewTabUrl: i.viewTabUrl,        colors: i.colors      })    }, n.src = i.dataURI;    else if ("getCapturePage" === i.msg) {      var n;      (n = new Image).onload = function() {        a.drawImage(n, 0, i.position * window.devicePixelRatio)      }, n.src = i.dataURI    } else "initializeCanvas" === i.msg ? (e.width = i.size.width * window.devicePixelRatio, e.height = i.size.height * window.devicePixelRatio) : "are_you_there_content_script?" === i.command && o({      status: "yes"    })  }))})();

Finish builds and posts the palette payload

sitePalette.js
buildPostColors: function() {  var e = {    cube: [],    thief: [],    vibrant: [],    css: []  };  for (var t in this.data)    for (var n in this.data[t].colors) {      var r = {        hex: this.data[t].colors[n].hex,        name: this.data[t].colors[n].name      };      "vibrant" == t && (r.vibrant_name = this.data[t].colors[n].vibrant_text), e[t].push(r)    }  return e}finish: function() {  var e = (new Date).getTime(),    t = "Done in " + this.toSeconds(e - this.timestamp).toString() + " sec";  this.debug && $("#profiler-done").html(t).show();  var n = this,    r = n.buildPostColors(),    o = {      title: new URL(n.url).hostname.replace(/^www\./, ""),      url: n.url,      image: n.imagesrc,      colors: r    };  document.getElementById("palette-title").textContent = `/ ${o.title}`, chrome.storage.local.get(["palette-token"]).then((e => {    Be(`${n.host}/palettes`, {      headers: {        Authorization: "Bearer " + e["palette-token"],        "Content-Type": "application/json"      },      method: "POST",      data: JSON.stringify(o)    }).then((e => {      e.data.message ? window.close() : (n.resultUrl = "https://palette.site/palette?id=" + e.data.palette._id, $(".js-get-share-link").removeClass("is-loading"))    })).catch((e => {      console.log("Something went wrong. Try again.")    }))  }))}
External service receiving the palette upload
    • api.palette.site

    Receives the palette POST containing the page URL, derived title, full-page JPEG data URL, generated colors, and Site Palette account authorization header.

Current tab URL sent when creating a palette

Clicking Site Palette's create button sends the active tab URL and hostname to api.palette.site/palettes.

Verification confirmed the popup-background-content-script path carrying the URL; no body recorded since creation wasn't triggered.

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

You click Site Palette's create button on the page you are viewing.

The popup only enables this flow for HTTP or HTTPS tabs.

The extension did this

The extension includes that page's URL and hostname in the palette record it sends to Palette's API.

The evidence traces the URL from the popup message through the background worker into the final POST body.

Fields placed in the palette creation object
  • Current page URL
    https://docs.example.com/design/colors?project=brand-refresh

    This identifies the exact page open when you create the palette, including any path or query string in the address.

  • Site hostname
    docs.example.com

    This records which site the palette came from, even when the full page path is not shown elsewhere.

  • Palette colors
    #1f2937, #f9fafb, #2563eb

    This describes the colors the extension extracted from the page and links the palette to the page URL sent with it.

Captured request
POSThttps://api.palette.site/palettes

No response body was recorded in the stored evidence.

Headers
Content-Type
application/json
Authorization
Bearer <redacted>
The code that does this

The active tab URL is collected before the palette POST is built

Readable version

popup.js sends command get-tab with tabUrl

popup.js
function v(t) {  chrome.runtime.sendMessage({    command: "get-tab",    tabId: t.id,    tabUrl: t.url  }), chrome.tabs.sendMessage(t.id, {    msg: "getPageDetails"  }, (function(t) {    g.classList.contains("container_hide") || g.classList.add("container_hide"), f.classList.contains("container_hide") && f.classList.remove("container_hide")  }))}a.addEventListener("click", (async function(t) {  const e = await L;  chrome.scripting.executeScript({    target: {      tabId: e.id    },    func: i.collectVisibleColors,    args: [{      maxColors: 10,      textWeight: .3,      samples: 1600    }]  }, (t => {    if (t && t[0]) {      const e = t[0].result;      chrome.runtime.sendMessage({        command: "viewport-colors-analyzed",        data: {          palette: e.palette        }      })    }    chrome.tabs.sendMessage(e.id, {      command: "are_you_there_content_script?"    }, (function(t) {      "yes" !== (t = t || {}).status ? chrome.scripting.executeScript({        target: {          tabId: e.id        },        files: ["scroller.js", "getPalette.js", "sitePalette.js"]      }, (() => {        v(e)      })) : "yes" === t.status && v(e), chrome.runtime.lastError && console.log("err", chrome.runtime.lastError)    }))  }))}))

sitePalette.js finish builds and posts the palette object

sitePalette.js
finish: function() {  var e = (new Date).getTime(),    t = "Done in " + this.toSeconds(e - this.timestamp).toString() + " sec";  this.debug && $("#profiler-done").html(t).show();  var n = this,    r = n.buildPostColors(),    o = {      title: new URL(n.url).hostname.replace(/^www\./, ""),      url: n.url,      image: n.imagesrc,      colors: r    };  document.getElementById("palette-title").textContent = `/ ${o.title}`, chrome.storage.local.get(["palette-token"]).then((e => {    Be(`${n.host}/palettes`, {      headers: {        Authorization: "Bearer " + e["palette-token"],        "Content-Type": "application/json"      },      method: "POST",      data: JSON.stringify(o)    }).then((e => {      e.data.message ? window.close() : (n.resultUrl = "https://palette.site/palette?id=" + e.data.palette._id, $(".js-get-share-link").removeClass("is-loading"))    })).catch((e => {      console.log("Something went wrong. Try again.")    }))  }))}
External destination receiving the palette request
    • api.palette.site

    Receives palette creation requests that include the active tab URL, hostname-derived title, page image data, colors, and a Bearer token.

Updated 30 September 2026pekhihjiehdafocefoimckjpbkegknoh