Is Storylane safe?
Storylane is medium risk. Starting a Storylane capture, v4.0.5.5 re-fetches images, fonts, CSS, and SVG refs with cookies included per origin. Resources over 5 KB upload via Storylane's API when CDN upload is on. Dynamic analysis didn't reach this; code-derived.
Who publishes itSTORYLANE INC - no other listings under this identity, 1 shared hostname
STORYLANE INC - no other listings under this identity, 1 shared hostname
What this publisher told the store about itself, and the other listings that told it the same thing.
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.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Storylane capture includes cookies in resource fetches
Starting a Storylane capture, v4.0.5.5 re-fetches images, fonts, CSS, and SVG refs with cookies included per origin.
Resources over 5 KB upload via Storylane's API when CDN upload is on.
Dynamic analysis didn't reach this; code-derived.
What actually happens
You start a Storylane capture on a page that references resources from other origins.
The extension re-fetches those resources with cookies included and can pass the resulting files to Storylane's upload flow.
This applies to resources the capture engine inlines, including image sources, CSS URLs, CSS imports, and SVG image references.
Fields and options in the affected fetch and upload path
- Resource URLhttps://assets.example-corp.com/secure/logo.png (illustrative)
This is the page resource the extension re-fetches during capture, even when it belongs to a different origin than the page being captured.
- Cookies includedcredentials: "include"
Cookies for the resource's own site can be attached to the re-fetch, exposing cookie-protected resources to the capture flow.
- Capture page headersReferer: https://app.example-corp.com/dashboard; Origin: https://app.example-corp.com (illustrative)
The fetch options add page context headers, so the request can carry information about where the capture started.
- Uploaded resource recordpayload: { data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", resourceURL: "https://assets.example-corp.com/secure/logo.png" } (illustrative)
When the CDN option accepts a resource, the encoded file and original resource URL are sent to the background upload handler.
Captured request
The code expects an upload signature with a pre-signed storage URL, then performs a PUT of the resource file to that URL.
- Content-Type
- application/json
- Authorization
- Bearer <redacted>
- Extension-Version
- 4.0.5.5
The code that does this
The capture engine fetches resources with cookies and forwards eligible files for upload
Readable resource fetch helper
deobfuscated/htmlCaptureEngineScript.bundle.js:1076-1115Ee = async (e, t) => { if (e.startsWith("data:")) return e; if (e.startsWith(chrome.runtime.getURL(""))) return ""; let n; try { n = await fetch(e, { ...t, credentials: "include" }) } catch (n) { return chrome.tabs ? "" : await chrome.runtime.sendMessage({ action: ve.inlineResource, payload: { url: e, fetchParams: t } }) } const r = await n.arrayBuffer(), o = Le({ url: e, responseContentType: n.headers.get("content-type"), buffer: r }); if (o?.startsWith("text/css")) return (new TextDecoder).decode(r); if (!o) return console.error(`Could not get content type for: ${e}`), ""; const i = e.split("#"); if ("image/svg+xml" === o && i.length > 1) { const e = (new TextDecoder).decode(r), t = i[1]; return `data:image/svg+xml;base64,${btoa(Ae(e,t))}` } if (o.startsWith("text/html")) return ""; const a = new FileReader; return a.readAsDataURL(new Blob([r], { type: o })), await new Promise(((e, t) => { a.addEventListener("load", (() => e(a.result)), !1), a.addEventListener("error", t, !1) }))}Readable CDN handoff for captured images and fonts
deobfuscated/htmlCaptureEngineScript.bundle.js:1153-1191const Fe = async function({ url: e, captureOptions: t}) { if (!e) return ""; const n = await Ee(e, { headers: { Referer: window.location.href, Origin: window.location.origin, "User-Agent": navigator.userAgent } }), r = n.match(/^data:([a-zA-Z0-9-+/]+\/[a-zA-Z0-9-+.]+);base64,/), o = r?.[1]; if (t?.cdnUploadFonts && ("font/otf" === o || "font/ttf" === o || "font/eot" === o || "font/woff" === o || "font/woff2" === o)) { const { cdnUrl: t } = await Me({ action: ne.webCaptureUploadCdnResource, payload: { data: n, resourceURL: e } }); return t || n } if (t?.cdnUploadImages && ("image/png" === o || "image/jpeg" === o || "image/avif" === o || "image/webp" === o || "image/gif" === o || "image/svg+xml" === o)) { const { cdnUrl: t } = await Me({ action: ne.webCaptureUploadCdnResource, payload: { data: n, resourceURL: e } }); return t || n } return n}Readable background upload handler
deobfuscated/background.bundle.js:23008-23035else if (e.action === Kd.webCaptureUploadCdnResource) { const { data: t, resourceURL: a } = e.payload, r = /^data:([a-zA-Z0-9-+/]+\/[a-zA-Z0-9-+.]+);base64,/, s = t.match(r), i = s?.[1]; if (!i) throw n({ cdnUrl: void 0 }), new Error("Missing content type for resource"); if ("font/woff" !== i && "font/woff2" !== i && "font/ttf" !== i && "font/otf" !== i && "image/png" !== i && "image/jpeg" !== i && "image/avif" !== i && "image/webp" !== i && "image/gif" !== i && "image/svg+xml" !== i && "application/x-font-ttf" !== i) return void n({ cdnUrl: void 0 }); if (t.length < 5120) return void n({ cdnUrl: void 0 }); try { const e = new URL(a); let r = await ic({ base64: t, contentType: i }); "image/svg+xml" === i && Boolean(e.hash) && (r += e.hash), n({ cdnUrl: r }) } catch (e) { n({ cdnUrl: void 0 }) }}Readable Storylane upload signature request
deobfuscated/background.bundle.js:17155-17224,18888-18917,22745-22759function hr(e, t = "get", n, a = {}) { return fetch(`https://api.storylane.io/api/${e}`, { method: t, credentials: "include", body: JSON.stringify(n), ...a, headers: { "Content-Type": "application/json", "Extension-Version": _r, ...a.headers } }).then((e => e.ok ? e.json() : e.json().then((t => { throw new Error(t.error.description || e.statusText) })))).catch((e => { if (!("object" == typeof e && e && "message" in e ? String(e.message) : "").includes("Unexpected end of JSON input")) throw console.error(e), e }))}function pr(e, t = "get", n, a) { return new Promise(((r, s) => { (async function() { return mr("https://app.storylane.io") })().then((i => { i ? r(hr(e, t, n, { headers: { Authorization: `Bearer ${i}` }, ...a })) : s(new Error(`User is not authenticated. Performing ${t} request to ${e}`)) })) }))}function Lr(e) { return pr("v1/company/aws_signatures", "post", e)}const vd = async ({ file: e, uploadKey: t, gzip: n}) => { const a = await (r = { key: t, filename: e.name }, $r((() => Lr(r)), cr.createAwsSignature, r)); var r; const { headers: s, url: i } = a, o = new URL(i), d = { "Access-Control-Allow-Origin": "*", "Content-Type": e.type, "Cache-Control": "no-cache" }; n && (e = await Yd(e), d["Content-Encoding"] = "gzip"); const u = await fetch(o.href, { method: "PUT", headers: { ...d, ...s }, body: e }); if (!u.ok) throw new Error(u.statusText); return `${o.origin}${o.pathname}`};ic = async ({ base64: e, contentType: t}) => { try { const n = await za.get(), a = await rc(e), r = await Ja(); if (r && r[a]) return r[a]; if (n) { const n = (await Dd({ file: Sd(e, a + sc(t)), uploadKey: "resources" })).replace("storylane-prod-uploads.s3.us-east-2.amazonaws.com", "app-content.storylane.io"), r = await Ja(); return await async function(e) { const t = await za.get(); if (t) return za.set({ ...t, cdnCachedResources: e }) }({ ...r, [a]: n }), n } return } catch (e) { return void console.error(e) }}Destinations used by the upload path
- api.storylane.io
Storylane API host used to request upload signatures and other extension API calls.
- storylane-prod-uploads.s3.us-east-2.amazonaws.com
Storage host used by the pre-signed upload URL before the code rewrites returned resource URLs.
- app-content.storylane.io
Storylane content host returned for cached uploaded resources after the S3 hostname replacement.