Is NotebookLM Tools safe?

High risk

NotebookLM Tools captures Google session tokens and email from NotebookLM pages and stores them to make authenticated API calls on the user's behalf.

When a user visits notebooklm.google.com, the extension injects a script that reads authentication tokens (SNlM0e, bl token, f.sid) and the user's email address from the page's WIZ_global_data object. These credentials are stored persistently in chrome.storage.local, keyed by email, and used by the extension's API client to send authenticated requests to NotebookLM's batchexecute endpoint — including operations that create, delete, and modify notebooks and sources.

YTuong.mev2.1.0Chrome Web Store
75Risk
Who publishes it

YTuong.me - 5 other listings from the same operator, 1 of them carrying a finding

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

Publisher
YTuong.me

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

Publishers can request a review.

Findings

NotebookLM Tools collects Google session fields

Opening NotebookLM injects a script reading a Google auth token and email into page localStorage; the content script forwards them to storage keyed by email.

No logged-in profile was available, but the injector matched the source shape.

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

You open a NotebookLM page while the extension is installed.

The extension is configured to run its content script on notebooklm.google.com.

The extension did this

The extension copies Google session fields from the page into browser extension storage.

It injects a page script, reads the page's global session data, relays it through localStorage, then stores the account record by email.

Fields copied from the NotebookLM page
  • Google auth token
    ya29.a0AfH6SMC8mN7kQw2rXbP4sL9vZ0pT6jE1rAaK (illustrative)

    This is a session credential from the signed-in Google page. If another component can read it, it can represent the active session.

  • Signed-in email
    alex.researcher@example.com (illustrative)

    This ties the collected session fields to your Google account identity.

  • NotebookLM app value
    boq_notebooklm-web-server_20260710.01_p0 (illustrative)

    This adds application context to the account record the extension saves.

  • Session side values
    bl=boq_notebooklm-web-server_20260710.01_p0; f.sid=-5821749081723342217 (illustrative)

    These values provide additional session context alongside the token and email.

  • Google account index
    0

    This indicates which signed-in Google account slot the page is using.

What's stored on your device

The extension keeps the collected session record locally under the Google email. Token values are redacted here as credentials.

Location
localStorage key YT_NLM, then chrome.storage.local account config
Contents
{  "YT_NLM": {    "bl": "boq_notebooklm-web-server_20260710.01_p0 (illustrative)",    "app": "boq_notebooklm-web-server_20260710.01_p0 (illustrative)",    "fsid": "-5821749081723342217 (illustrative)",    "email": "alex.researcher@example.com (illustrative)",    "token": "<redacted>",    "authuser": 0  },  "YT_NOTEBOOKLM_TOOLS_CONFIG_NLM_ACCOUNTS": {    "accounts": {      "alex.researcher@example.com": {        "bl": "boq_notebooklm-web-server_20260710.01_p0 (illustrative)",        "app": "boq_notebooklm-web-server_20260710.01_p0 (illustrative)",        "fsid": "-5821749081723342217 (illustrative)",        "email": "alex.researcher@example.com (illustrative)",        "token": "<redacted>",        "authuser": 0,        "lastRefreshed": "2026-07-12T05:11:13.756Z"      }    },    "accountOrder": [      "alex.researcher@example.com"    ],    "activeAccount": "alex.researcher@example.com"  }}
The code that does this

Shipped code path that collects and stores the session fields

Readable version

Page script reads WIZ_global_data and writes localStorage

deobfuscated/inject.js
try {  var YTUONG_NLM_PAYLOAD = {}  // SNlM0e: token  if (window?.WIZ_global_data?.SNlM0e) {    YTUONG_NLM_PAYLOAD.token = window?.WIZ_global_data?.SNlM0e  }  // qwAQke: app  if (window?.WIZ_global_data?.qwAQke) {    YTUONG_NLM_PAYLOAD.app = window?.WIZ_global_data?.qwAQke  }  // cfb2h: bl  if (window?.WIZ_global_data?.cfb2h) {    YTUONG_NLM_PAYLOAD.bl = window?.WIZ_global_data?.cfb2h  }  // FdrFJe: f.sid  if (window?.WIZ_global_data?.FdrFJe) {    YTUONG_NLM_PAYLOAD.fsid = window?.WIZ_global_data?.FdrFJe  }  // oPEP7c: email  if (window?.WIZ_global_data?.oPEP7c) {    YTUONG_NLM_PAYLOAD.email = window?.WIZ_global_data?.oPEP7c  }  // Extract authuser from URL  var urlParams = new URLSearchParams(window.location.search)  YTUONG_NLM_PAYLOAD.authuser = parseInt(urlParams.get('authuser') || '0', 10) || 0  window.localStorage.setItem('YT_NLM', JSON.stringify(YTUONG_NLM_PAYLOAD))} catch (e) {  console.error('[NLM Tools] Failed to extract auth data:', e)}

Content script injects the page script and forwards the payload

deobfuscated/assets/chunk-6b4dd0be.js
(function(e, i) {  console.log("NotebookLM inject"), s().then(async () => {    let o = {};    try {      const r = window.localStorage.getItem("YT_NLM");      r && (o = JSON.parse(r));    } catch (r) {      console.error("[NLM Tools] Corrupted auth storage:", r), window.localStorage.removeItem("YT_NLM");    }    try {      const r = await N("YTUONG_NLM_PAYLOAD", o, "background");      console.log("Response from background script:", r);    } catch (r) {      console.error("[NLM Tools] Failed to send auth payload:", r);    }    const t = (await chrome.storage.local.get("YT_NOTEBOOKLM_TOOLS_INTERFACE_LANGUAGE")).YT_NOTEBOOKLM_TOOLS_INTERFACE_LANGUAGE;    if (t && new URL(window.location.href).searchParams.get("hl") !== t) {      console.log("Auto-applying interface language:", t), a(t);      return;    }  }).catch((o) => {    console.error("Script injection failed:", o);  }), e.runtime.onMessage.addListener((o, n, t) => {    switch (o.type) {      case "RELOAD_PAGE":        window.location.reload();        break;      case "UPDATE_INTERFACE_LANGUAGE":        a(o.languageCode);        break;    }  });  function a(o) {    const n = new URL(window.location.href);    n.searchParams.set("hl", o), n.toString() !== window.location.href && (window.location.href = n.toString());  }  function s() {    return new Promise((o, n) => {      var t = document.createElement("script");      t.src = e.runtime.getURL("inject.js"), t.onload = function() {        console.log("Inject script loaded successfully"), this.remove(), o();      }, t.onerror = function() {        console.error("Failed to load inject script"), this.remove(), n(new Error("Script loading failed"));      }, (document.head || document.documentElement).appendChild(t);    });  }})(chrome);

Background handler stores account payloads keyed by email

deobfuscated/assets/chunk-6eba50c8.js
h("YTUONG_NLM_PAYLOAD", async ({ data: e }) => {  try {    if (!(e == null ? void 0 : e.token) || !(e == null ? void 0 : e.email))      return console.warn("[NLM Tools] Invalid payload, skipping"), { success: false, error: "Invalid payload" };    const n = await d.get(m) || {}, t = { ...n.accounts || {} };    t[e.email] = { ...e, lastRefreshed: (/* @__PURE__ */ new Date()).toISOString() };    const o = n.activeAccount || e.email, a = n.accountOrder ? [...n.accountOrder] : [];    a.includes(e.email) || a.push(e.email);    const r = { accounts: t, activeAccount: o, accountOrder: a };    return await d.set(m, r), t[o] && await d.set(C, t[o]), await v(), { success: true };  } catch (n) {    return console.error("[NLM Tools] Failed to store payload:", n), { success: false, error: n.message };  }});
The code that does this

NotebookLM page scope declared in the manifest

Readable version
{  "content_scripts": [    {      "js": [        "assets/index.js-loader-56a17ce3.js",        "assets/content.jsx-loader-8a8227ba.js"      ],      "matches": [        "*://notebooklm.google.com/*"      ]    }  ],  "web_accessible_resources": [    {      "matches": [        "*://notebooklm.google.com/*"      ],      "resources": [        "*",        "assets/chunk-38df7042.js",        "assets/chunk-9bfc7485.js",        "assets/chunk-725317a4.js",        "assets/chunk-6b4dd0be.js",        "assets/chunk-c4a3a80c.js",        "assets/chunk-b8cdbf8b.js",        "assets/chunk-4ca06da3.js",        "assets/chunk-a12ead9f.js",        "assets/chunk-8db09c21.js",        "assets/chunk-dd74c6e0.js",        "assets/chunk-8d3650e6.js",        "assets/chunk-d45ab30c.js",        "assets/chunk-f676d28d.js"      ],      "use_dynamic_url": false    }  ],  "host_permissions": [    "*://notebooklm.google.com/*",    "http://*/*",    "https://*/*"  ]}

Google token and email stored in extension storage

Opening NotebookLM pages, the extension injects a script that reads Google page-data for a token and account email, sends it to the background worker, and stores it under an accounts object keyed by email in extension storage.

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

You open a NotebookLM page where the extension is allowed to run.

The extension did this

The extension reads Google page-data values for your token and email, then stores the account record in extension storage.

Fields copied from Google page data and stored by the extension
  • Google token
    ya29.a0AfH6SMAq6mJfP8YwR9pK2xQ7L3nB5cD4eF6gH7iJ8kL9mN0pQ (illustrative)

    This value is tied to your Google-backed NotebookLM session and is stored by the extension instead of staying only in the page.

  • Google account email
    alex.chen@example.com (illustrative)

    This ties the stored token record to your Google account and is also used as the lookup key for the account list.

  • Account selector number
    1 (illustrative)

    This records which signed-in Google profile the page was using when the token was collected.

  • Refresh timestamp
    2026-07-12T05:12:25.956Z (illustrative)

    This records when the extension last refreshed the stored account record.

The code that does this

The page-injected script copies token and email from Google page data

Readable version

Readable view of the same page script

inject.js
try {  var YTUONG_NLM_PAYLOAD = {}  // SNlM0e: token  if (window?.WIZ_global_data?.SNlM0e) {    YTUONG_NLM_PAYLOAD.token = window?.WIZ_global_data?.SNlM0e  }  // qwAQke: app  if (window?.WIZ_global_data?.qwAQke) {    YTUONG_NLM_PAYLOAD.app = window?.WIZ_global_data?.qwAQke  }  // cfb2h: bl  if (window?.WIZ_global_data?.cfb2h) {    YTUONG_NLM_PAYLOAD.bl = window?.WIZ_global_data?.cfb2h  }  // FdrFJe: f.sid  if (window?.WIZ_global_data?.FdrFJe) {    YTUONG_NLM_PAYLOAD.fsid = window?.WIZ_global_data?.FdrFJe  }  // oPEP7c: email  if (window?.WIZ_global_data?.oPEP7c) {    YTUONG_NLM_PAYLOAD.email = window?.WIZ_global_data?.oPEP7c  }  // Extract authuser from URL  var urlParams = new URLSearchParams(window.location.search)  YTUONG_NLM_PAYLOAD.authuser = parseInt(urlParams.get('authuser') || '0', 10) || 0  window.localStorage.setItem('YT_NLM', JSON.stringify(YTUONG_NLM_PAYLOAD))} catch (e) {  console.error('[NLM Tools] Failed to extract auth data:', e)}
The code that does this

The content script forwards the payload to the background service worker

Readable version

Readable view of the same bridge code

assets/chunk-6b4dd0be.js
(function(e, i) {  console.log("NotebookLM inject"), s().then(async () => {    let o = {};    try {      const r = window.localStorage.getItem("YT_NLM");      r && (o = JSON.parse(r));    } catch (r) {      console.error("[NLM Tools] Corrupted auth storage:", r), window.localStorage.removeItem("YT_NLM");    }    try {      const r = await N("YTUONG_NLM_PAYLOAD", o, "background");      console.log("Response from background script:", r);    } catch (r) {      console.error("[NLM Tools] Failed to send auth payload:", r);    }    const t = (await chrome.storage.local.get("YT_NOTEBOOKLM_TOOLS_INTERFACE_LANGUAGE")).YT_NOTEBOOKLM_TOOLS_INTERFACE_LANGUAGE;    if (t && new URL(window.location.href).searchParams.get("hl") !== t) {      console.log("Auto-applying interface language:", t), a(t);      return;    }  }).catch((o) => {    console.error("Script injection failed:", o);  }), e.runtime.onMessage.addListener((o, n, t) => {    switch (o.type) {      case "RELOAD_PAGE":        window.location.reload();        break;      case "UPDATE_INTERFACE_LANGUAGE":        a(o.languageCode);        break;    }  });  function a(o) {    const n = new URL(window.location.href);    n.searchParams.set("hl", o), n.toString() !== window.location.href && (window.location.href = n.toString());  }  function s() {    return new Promise((o, n) => {      var t = document.createElement("script");      t.src = e.runtime.getURL("inject.js"), t.onload = function() {        console.log("Inject script loaded successfully"), this.remove(), o();      }, t.onerror = function() {        console.error("Failed to load inject script"), this.remove(), n(new Error("Script loading failed"));      }, (document.head || document.documentElement).appendChild(t);    });  }})(chrome);
The code that does this

The background handler stores account records keyed by email

Readable version

Readable view of the background storage handler

assets/chunk-6eba50c8.js
h("YTUONG_NLM_PAYLOAD", async ({ data: e }) => {  try {    if (!(e == null ? void 0 : e.token) || !(e == null ? void 0 : e.email))      return console.warn("[NLM Tools] Invalid payload, skipping"), { success: false, error: "Invalid payload" };    const n = await d.get(m) || {}, t = { ...n.accounts || {} };    t[e.email] = { ...e, lastRefreshed: (/* @__PURE__ */ new Date()).toISOString() };    const o = n.activeAccount || e.email, a = n.accountOrder ? [...n.accountOrder] : [];    a.includes(e.email) || a.push(e.email);    const r = { accounts: t, activeAccount: o, accountOrder: a };    return await d.set(m, r), t[o] && await d.set(C, t[o]), await v(), { success: true };  } catch (n) {    return console.error("[NLM Tools] Failed to store payload:", n), { success: false, error: n.message };  }});
Updated 30 September 2026hiibkpjljigehlnnecbgehkhfibmahjn