Is Storylane safe?

Medium risk

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.

Storylane Incv4.0.5.6Chrome Web Store
45Risk

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

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You start a Storylane capture on a page that references resources from other origins.

The extension did this

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.

02EvidenceFIELD TABLE
Fields and options in the affected fetch and upload path
FieldValueWhy it matters
Resource URL
https://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 included
credentials: "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 headers
Referer: 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 record
payload: { 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.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.storylane.io/api/v1/company/aws_signatures
The code expects an upload signature with a pre-signed storage URL, then performs a PUT of the resource file to that URL.
Headers
Content-Typeapplication/json
AuthorizationBearer <redacted>
Extension-Version4.0.5.5
04EvidenceCODE COMPARE
The code that does this

The capture engine fetches resources with cookies and forwards eligible files for upload

What it actually does
Readable resource fetch helperdeobfuscated/htmlCaptureEngineScript.bundle.js:1076-1115
Ee = 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 fontsdeobfuscated/htmlCaptureEngineScript.bundle.js:1153-1191
const 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 handlerdeobfuscated/background.bundle.js:23008-23035
else 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 requestdeobfuscated/background.bundle.js:17155-17224,18888-18917,22745-22759
function 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)
  }
}
05EvidenceTHIRD PARTY LIST
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.

Updated 17 September 2026ongmhighpnfpojfidgigcjflgdkjfdeb