Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,26 @@ Gmail, Drive, YouTube, Meet, Calendar, Sheets, Gemini
## ⭐ If you like BookmarkFolder

Give the repo a **star** 🌟 on GitHub — it helps a lot!


## ☁️ Firebase Cloud Backup (optional)

You can now back up and restore your bookmarks using Firebase so reinstalling the extension won’t lose your data.

1. Create a Firebase project
2. Enable **Authentication → Email/Password**
3. Enable **Firestore Database** (start in test mode while developing)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

"Start in test mode" advice is a security risk.

Firestore test mode allows unauthenticated reads and writes to all data for 30 days. Users who follow this advice and forget to update rules will expose all stored bookmark data (including any data from other users). At minimum, add a warning and provide recommended production security rules.

📝 Suggested documentation improvement
-3. Enable **Firestore Database** (start in test mode while developing)
+3. Enable **Firestore Database** and set security rules (see below)

Then expand the security note at the end:

-> Security note: set Firestore security rules so users can only read/write their own doc.
+> **Security rules (required):** Apply the following Firestore rules so each user can only access their own document:
+>
+> ```
+> rules_version = '2';
+> service cloud.firestore {
+>   match /databases/{database}/documents {
+>     match /bookmarkfolderUsers/{userId} {
+>       allow read, write: if request.auth != null && request.auth.uid == userId;
+>     }
+>   }
+> }
+> ```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
3. Enable **Firestore Database** (start in test mode while developing)
3. Enable **Firestore Database** and set security rules (see below)
🤖 Prompt for AI Agents
In `@README.md` at line 81, Update the README's "Enable Firestore Database"
instruction to remove the blanket "start in test mode" advice or prepend a
prominent security warning about test mode allowing unauthenticated reads/writes
for 30 days, and add a recommended production rules example for the
bookmarkfolderUsers collection that enforces request.auth != null and
request.auth.uid == userId (place the full rules snippet in a fenced code block
and label it as production rules). Ensure the warning appears immediately next
to the Firestore setup step and include a short note reminding developers to
deploy rules before leaving development.

4. Open the new tab page and enter:
- Firebase Web API Key
- Firebase Project ID
5. Sign up / sign in from the sidebar panel
6. Use **Backup to Cloud** and **Restore from Cloud**

### Firestore document path used
`bookmarkfolderUsers/{firebaseLocalUserId}`

Each document stores:
- `data` (stringified JSON payload)
- `updatedAt` (unix timestamp in ms)

> Security note: set Firestore security rules so users can only read/write their own doc.
12 changes: 8 additions & 4 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
"manifest_version": 3,
"name": "Rishnu Dk-custom bookmark tab",
"version": "1.0",

"permissions": ["storage"],

"permissions": [
"storage"
],
"chrome_url_overrides": {
"newtab": "newtab.html"
}
},
"host_permissions": [
"https://identitytoolkit.googleapis.com/*",
"https://firestore.googleapis.com/*"
]
}
23 changes: 23 additions & 0 deletions newtab.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,29 @@ <h1>Bookmarks</h1>
<div class="sidebar">
<h2>Google Apps</h2>
<div id="googleApps" class="apps"></div>

<div class="cloud-panel">
<h3>Firebase Backup</h3>

<input id="firebaseApiKey" type="text" placeholder="Firebase Web API Key">
<input id="firebaseProjectId" type="text" placeholder="Firebase Project ID">

<input id="authEmail" type="email" placeholder="Email">
<input id="authPassword" type="password" placeholder="Password">
Comment on lines +31 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inputs lack <label> elements — accessibility concern.

Screen readers may not announce placeholder-only inputs correctly, and placeholders disappear once the user starts typing, making it hard to recall what each field expects. Add visible or sr-only labels for each input.

♿ Proposed fix (example for one input)
+       <label for="firebaseApiKey">Firebase Web API Key</label>
        <input id="firebaseApiKey" type="text" placeholder="Firebase Web API Key">
+       <label for="firebaseProjectId">Firebase Project ID</label>
        <input id="firebaseProjectId" type="text" placeholder="Firebase Project ID">
+       <label for="authEmail">Email</label>
        <input id="authEmail" type="email" placeholder="Email">
+       <label for="authPassword">Password</label>
        <input id="authPassword" type="password" placeholder="Password">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input id="firebaseApiKey" type="text" placeholder="Firebase Web API Key">
<input id="firebaseProjectId" type="text" placeholder="Firebase Project ID">
<input id="authEmail" type="email" placeholder="Email">
<input id="authPassword" type="password" placeholder="Password">
<label for="firebaseApiKey">Firebase Web API Key</label>
<input id="firebaseApiKey" type="text" placeholder="Firebase Web API Key">
<label for="firebaseProjectId">Firebase Project ID</label>
<input id="firebaseProjectId" type="text" placeholder="Firebase Project ID">
<label for="authEmail">Email</label>
<input id="authEmail" type="email" placeholder="Email">
<label for="authPassword">Password</label>
<input id="authPassword" type="password" placeholder="Password">
🤖 Prompt for AI Agents
In `@newtab.html` around lines 31 - 35, Add accessible labels for each input
element (firebaseApiKey, firebaseProjectId, authEmail, authPassword) by adding
corresponding <label> elements with for attributes matching each input id;
labels may be visually shown or use an sr-only CSS class for screen-reader-only
text so placeholders are not the only accessible hint for screen readers and
users who forget the placeholder once typing begins. Ensure label text clearly
describes the field (e.g., "Firebase API Key", "Firebase Project ID", "Email",
"Password") and that each label-input pair matches by id to maintain proper
associations.


<div class="cloud-actions">
<button id="signUpBtn" class="small-btn">Sign Up</button>
<button id="signInBtn" class="small-btn">Sign In</button>
<button id="signOutBtn" class="small-btn">Sign Out</button>
</div>

<div class="cloud-actions">
<button id="backupNowBtn" class="small-btn">Backup to Cloud</button>
<button id="restoreNowBtn" class="small-btn">Restore from Cloud</button>
</div>

<p id="cloudStatus" class="cloud-status">Not connected</p>
</div>
</div>

</div> <!-- END CONTAINER -->
Expand Down
214 changes: 211 additions & 3 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ let openFolders = {};
let dragSourceTile = null;
let dragPlaceholder = null;

let firebaseConfigState = { apiKey: "", projectId: "" };
let firebaseAuthState = { idToken: "", localId: "", email: "" };
let lastUpdatedAt = Date.now();
Comment on lines +13 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Firebase auth token stored in chrome.storage.sync — consider chrome.storage.local instead.

chrome.storage.sync replicates data across all signed-in Chrome instances. Storing idToken (a bearer credential) in sync storage means it will be synced to every device the user is signed into Chrome on. Use chrome.storage.local for sensitive auth state, or at minimum store only the non-sensitive config (apiKey, projectId) in sync and keep tokens local.

🤖 Prompt for AI Agents
In `@script.js` around lines 13 - 15, The firebaseAuthState object (idToken,
localId, email) is sensitive and must not be saved to chrome.storage.sync;
update any persistence/read logic that currently writes or reads
firebaseAuthState to use chrome.storage.local instead, while leaving
firebaseConfigState (apiKey, projectId) in chrome.storage.sync if desired;
ensure idToken is never written to sync storage and migrate/remove any existing
synced tokens (use lastUpdatedAt to coordinate migration/cleanup).


// ---------------------------------------------------------
// LOAD EVERYTHING
// ---------------------------------------------------------
function loadAll() {

chrome.storage.sync.get(["bookmarks", "folders", "openFolders"], res => {
chrome.storage.sync.get(["bookmarks", "folders", "openFolders", "firebaseConfig", "firebaseAuth", "lastUpdatedAt"], async res => {

const raw = res.bookmarks || [];

Expand All @@ -35,11 +39,18 @@ function loadAll() {
}
});

chrome.storage.sync.set({ bookmarks, folders, openFolders });
firebaseConfigState = res.firebaseConfig || { apiKey: "", projectId: "" };
lastUpdatedAt = res.lastUpdatedAt || Date.now();
firebaseAuthState = res.firebaseAuth || { idToken: "", localId: "", email: "" };

chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt });
Comment on lines +42 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

lastUpdatedAt defaults to Date.now() on first load, which may prevent cloud restore.

Line 43: if lastUpdatedAt is not yet stored, it defaults to the current time. When a user installs the extension on a new device and signs in, the freshly-initialized lastUpdatedAt will be "now", which is newer than any existing cloud backup. The timestamp comparison in restoreFromCloud (line 773) will then skip the restore, defeating the purpose.

Default to 0 instead so cloud data is always preferred on a fresh install.

🐛 Proposed fix
-    lastUpdatedAt = res.lastUpdatedAt || Date.now();
+    lastUpdatedAt = res.lastUpdatedAt || 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
firebaseConfigState = res.firebaseConfig || { apiKey: "", projectId: "" };
lastUpdatedAt = res.lastUpdatedAt || Date.now();
firebaseAuthState = res.firebaseAuth || { idToken: "", localId: "", email: "" };
chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt });
firebaseConfigState = res.firebaseConfig || { apiKey: "", projectId: "" };
lastUpdatedAt = res.lastUpdatedAt || 0;
firebaseAuthState = res.firebaseAuth || { idToken: "", localId: "", email: "" };
chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt });
🤖 Prompt for AI Agents
In `@script.js` around lines 42 - 46, The code sets lastUpdatedAt =
res.lastUpdatedAt || Date.now(), which makes a fresh install appear newer than
cloud backups and prevents restore; change the default fallback to 0 instead
(i.e., set lastUpdatedAt = res.lastUpdatedAt ?? 0) so that when no stored
timestamp exists cloud data is preferred, and keep the chrome.storage.sync.set
call (which writes firebaseConfigState, firebaseAuthState, lastUpdatedAt) so the
updated default is persisted; check restoreFromCloud to rely on this 0 default
for initial restores.


renderFolders();
populateFolderSelect();
renderGoogleApps();
initializeCloudUI();

await restoreFromCloud(false);
});
}
loadAll();
Expand All @@ -48,7 +59,9 @@ loadAll();
// SAVE ALL
// ---------------------------------------------------------
function saveAll() {
chrome.storage.sync.set({ bookmarks, folders, openFolders });
lastUpdatedAt = Date.now();
chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt });
backupToCloud(false);
}

// ---------------------------------------------------------
Expand Down Expand Up @@ -584,3 +597,198 @@ function renderGoogleApps() {
div.appendChild(item);
});
}


// ---------------------------------------------------------
// FIREBASE CLOUD BACKUP (REST API)
// ---------------------------------------------------------
function initializeCloudUI() {
const apiKeyInput = document.getElementById("firebaseApiKey");
const projectIdInput = document.getElementById("firebaseProjectId");
const emailInput = document.getElementById("authEmail");
const passwordInput = document.getElementById("authPassword");

if (!apiKeyInput || !projectIdInput) return;

apiKeyInput.value = firebaseConfigState.apiKey || "";
projectIdInput.value = firebaseConfigState.projectId || "";
emailInput.value = firebaseAuthState.email || "";

apiKeyInput.addEventListener("change", () => {
firebaseConfigState.apiKey = apiKeyInput.value.trim();
persistCloudState();
setCloudStatus("Firebase config updated.");
});

projectIdInput.addEventListener("change", () => {
firebaseConfigState.projectId = projectIdInput.value.trim();
persistCloudState();
setCloudStatus("Firebase config updated.");
});

document.getElementById("signUpBtn")?.addEventListener("click", () => authWithFirebase("signUp"));
document.getElementById("signInBtn")?.addEventListener("click", () => authWithFirebase("signInWithPassword"));
document.getElementById("signOutBtn")?.addEventListener("click", signOutFirebase);
document.getElementById("backupNowBtn")?.addEventListener("click", () => backupToCloud(true));
document.getElementById("restoreNowBtn")?.addEventListener("click", () => restoreFromCloud(true));

updateCloudStatusFromState();
}

function persistCloudState() {
chrome.storage.sync.set({ firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState });
}

function setCloudStatus(message, isError = false) {
const status = document.getElementById("cloudStatus");
if (!status) return;
status.textContent = message;
status.style.color = isError ? "#fca5a5" : "#93c5fd";
}

function updateCloudStatusFromState() {
if (firebaseAuthState.email) {
setCloudStatus(`Connected as ${firebaseAuthState.email}`);
} else {
setCloudStatus("Not connected");
}
}

function cloudConfigReady() {
return Boolean(firebaseConfigState.apiKey && firebaseConfigState.projectId);
}

function cloudAuthReady() {
return Boolean(firebaseAuthState.idToken && firebaseAuthState.localId);
}

function cloudPayload() {
return {
bookmarks,
folders,
openFolders,
updatedAt: lastUpdatedAt
};
}

async function authWithFirebase(mode) {
const email = document.getElementById("authEmail")?.value.trim();
const password = document.getElementById("authPassword")?.value.trim();

if (!cloudConfigReady()) return setCloudStatus("Add Firebase API Key + Project ID first.", true);
if (!email || !password) return setCloudStatus("Enter email and password.", true);

try {
const endpoint = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${firebaseConfigState.apiKey}`;
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, returnSecureToken: true })
});

const data = await res.json();
if (!res.ok) throw new Error(data.error?.message || "Authentication failed");

firebaseAuthState = { idToken: data.idToken, localId: data.localId, email: data.email || email };
persistCloudState();
updateCloudStatusFromState();
await restoreFromCloud(false);
} catch (err) {
setCloudStatus(`Auth failed: ${err.message}`, true);
}
}
Comment on lines +674 to +699
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Firebase Identity Toolkit API refresh token endpoint documentation

💡 Result:

Firebase (Identity Toolkit / Firebase Auth) refresh-token exchange is documented under “Exchange a refresh token for an ID token” and uses the Secure Token API:

  • HTTP method: POST
  • Endpoint: https://securetoken.googleapis.com/v1/token?key=[API_KEY]
  • Content-Type: application/x-www-form-urlencoded
  • Body fields:
    • grant_type=refresh_token (always)
    • refresh_token=[REFRESH_TOKEN] (the user’s Firebase refresh token)
  • Response includes: id_token, refresh_token, expires_in, token_type, user_id, project_id [1] (also documented for Identity Platform) [2]

Example (from the docs):

curl 'https://securetoken.googleapis.com/v1/token?key=[API_KEY]' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'grant_type=refresh_token&refresh_token=[REFRESH_TOKEN]'

Sources: Firebase Auth REST API docs [1], Google Cloud Identity Platform REST docs [2].


🏁 Script executed:

# Search for refreshToken in the codebase
rg "refreshToken" -i --type js

# Check the firebaseAuthState structure and its usage
rg "firebaseAuthState" --type js -A 3 -B 1

# Look for token-related code around the auth function
rg "idToken|token" script.js -i -B 2 -A 2

Repository: rishnudk/BookmarkFolder

Length of output: 4641


🏁 Script executed:

# Check for 401 error handling in cloud operations
rg "401|unauthorized|token.*expire|Bearer" script.js -i -B 2 -A 2

# Look at the full backupToCloud and restoreFromCloud functions
rg "async function (backupToCloud|restoreFromCloud)" -A 30 script.js

Repository: rishnudk/BookmarkFolder

Length of output: 2294


🌐 Web query:

Firebase ID token expiration duration hours

💡 Result:

Firebase Authentication ID tokens expire after 1 hour (≈ 3600 seconds) — i.e., 1 hour. The client SDKs automatically refresh them (using the refresh token) about every hour. [1], [2]

(If you meant custom tokens you mint server-side: their exp can be at most 1 hour after iat.) [5]


🌐 Web query:

Firebase Identity Toolkit signupNewUser signin REST API response format refreshToken

💡 Result:

For Firebase Auth / Identity Toolkit REST (Identity Platform), the refreshToken is returned in the JSON response when you send returnSecureToken: true.

signupNewUser (aka accounts:signUp)

Endpoint: POST https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=API_KEY

Response JSON (success):

{
  "kind": "identitytoolkit#SignupNewUserResponse",
  "idToken": "",
  "email": "user@example.com",
  "refreshToken": "",
  "expiresIn": "3600",
  "localId": ""
}

(expiresIn is a string containing the number of seconds until the ID token expires.) [1][2]

signin (aka accounts:signInWithPassword)

Endpoint: POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=API_KEY

Response JSON (success):

{
  "kind": "identitytoolkit#VerifyPasswordResponse",
  "localId": "",
  "email": "user@example.com",
  "displayName": "",
  "idToken": "",
  "registered": true,
  "refreshToken": "",
  "expiresIn": "3600"
}
``` [2][3]

If you’re not seeing `refreshToken` in responses, it’s typically because `returnSecureToken` was omitted or set to `false`. [2]

**Sources:** [1] SignUpResponse schema, [2] “Using the REST API” (sample requests/responses), [3] `accounts.signInWithPassword` reference response schema.

---



</details>

**Store `refreshToken` and implement token refresh before cloud operations.**

`authWithFirebase` receives and discards the `refreshToken` returned by the Identity Toolkit API. Since ID tokens expire after 1 hour, all cloud operations (`backupToCloud`, `restoreFromCloud`) will fail with a 401 when `firebaseAuthState.idToken` becomes invalid. The current error handling will mask this as a generic "Backup failed" or "Restore failed" message without recovery.

Store `data.refreshToken` in `firebaseAuthState` and refresh the token using `https://securetoken.googleapis.com/v1/token` (with `grant_type=refresh_token`) before making cloud calls, or detect 401 responses and prompt re-authentication.

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @script.js around lines 674 - 699, The authWithFirebase function currently
drops data.refreshToken; modify it to persist refreshToken into
firebaseAuthState (e.g., firebaseAuthState.refreshToken) when setting
idToken/localId/email and call persistCloudState(); then implement a token
refresh helper (e.g., refreshFirebaseIdToken) that POSTs to
https://securetoken.googleapis.com/v1/token with grant_type=refresh_token and
the stored refreshToken to obtain a new id_token and refresh_token, update
firebaseAuthState and persist; invoke this helper before cloud operations
(backupToCloud, restoreFromCloud) or on 401 responses from cloud endpoints to
automatically refresh tokens and retry the request, falling back to prompting
re-authentication if refresh fails.


</details>

<!-- fingerprinting:phantom:medusa:phoenix -->

<!-- This is an auto-generated comment by CodeRabbit -->


function signOutFirebase() {
firebaseAuthState = { idToken: "", localId: "", email: "" };
persistCloudState();
updateCloudStatusFromState();
}

function cloudDocumentUrl() {
return `https://firestore.googleapis.com/v1/projects/${firebaseConfigState.projectId}/databases/(default)/documents/bookmarkfolderUsers/${firebaseAuthState.localId}`;
}

async function backupToCloud(showMessage = true) {
if (!cloudConfigReady() || !cloudAuthReady()) {
if (showMessage) setCloudStatus("Cloud backup skipped: sign in first.", true);
return;
}

try {
const payload = cloudPayload();
const body = {
fields: {
data: { stringValue: JSON.stringify(payload) },
updatedAt: { integerValue: String(payload.updatedAt) }
}
};

const res = await fetch(cloudDocumentUrl(), {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${firebaseAuthState.idToken}`
},
body: JSON.stringify(body)
});

const data = await res.json();
if (!res.ok) throw new Error(data.error?.message || "Backup failed");

if (showMessage) setCloudStatus("Backup complete.");
} catch (err) {
setCloudStatus(`Backup failed: ${err.message}`, true);
}
}

async function restoreFromCloud(showMessage = true) {
if (!cloudConfigReady() || !cloudAuthReady()) {
if (showMessage) setCloudStatus("Cloud restore skipped: sign in first.", true);
return;
}

try {
const res = await fetch(cloudDocumentUrl(), {
headers: { Authorization: `Bearer ${firebaseAuthState.idToken}` }
});

if (res.status === 404) {
if (showMessage) setCloudStatus("No cloud backup found yet.");
return;
}

const data = await res.json();
if (!res.ok) throw new Error(data.error?.message || "Restore failed");

const encoded = data.fields?.data?.stringValue;
if (!encoded) {
if (showMessage) setCloudStatus("Cloud document is empty.", true);
return;
}

const cloud = JSON.parse(encoded);
const localUpdatedAt = Number(lastUpdatedAt || 0);
const cloudUpdatedAt = Number(cloud.updatedAt || 0);

if (cloudUpdatedAt < localUpdatedAt) {
if (showMessage) setCloudStatus("Local data is newer than cloud.");
return;
}

bookmarks = Array.isArray(cloud.bookmarks) ? cloud.bookmarks : [];
folders = Array.isArray(cloud.folders) && cloud.folders.length ? cloud.folders : ["Default"];
openFolders = cloud.openFolders && typeof cloud.openFolders === "object" ? cloud.openFolders : {};

folders.forEach(f => {
if (openFolders[f] === undefined) openFolders[f] = f === "Default";
});

saveAll();
renderFolders();
populateFolderSelect();
Comment on lines +769 to +788
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

XSS risk: cloud-restored data is rendered via innerHTML without sanitization.

restoreFromCloud parses arbitrary JSON from Firestore (line 769) and assigns it directly to bookmarks and folders. These values are later interpolated into innerHTML (e.g., line 122: <span class="folder-name">${folder}</span> and line 217: <div class="tile-name">${b.name}</div>).

A compromised or tampered Firestore document could inject malicious HTML/JS into the page. Sanitize or escape all user-controlled strings before DOM insertion, or switch to textContent/createElement for rendering.

🛡️ Minimal fix — escape HTML in rendered values

Add a utility:

function escapeHTML(str) {
  const div = document.createElement("div");
  div.textContent = str;
  return div.innerHTML;
}

Then use it in renderFolders:

-      <span class="folder-name">${folder}</span>
+      <span class="folder-name">${escapeHTML(folder)}</span>
-        <div class="tile-name">${b.name}</div>
+        <div class="tile-name">${escapeHTML(b.name)}</div>

Also escape b.icon in the <img src=...> or validate it's a proper URL.

🤖 Prompt for AI Agents
In `@script.js` around lines 769 - 788, The restoreFromCloud flow assigns
untrusted JSON into bookmarks/folders/openFolders (in restoreFromCloud) and
those values are later rendered via innerHTML in renderFolders and
populateFolderSelect; to fix, ensure all user-controlled strings are escaped or
inserted as text nodes before any DOM insertion: add/use an escapeHTML utility
(or prefer element.textContent/createElement and appendChild) when rendering
folder names, bookmark names, descriptions and any interpolated fields, and
validate/sanitize b.icon before setting it as an image src; update
renderFolders, populateFolderSelect and any code that builds HTML from
bookmarks/folders to use the sanitizer or textContent approach instead of raw
innerHTML.


if (showMessage) setCloudStatus("Restore complete.");
} catch (err) {
setCloudStatus(`Restore failed: ${err.message}`, true);
}
Comment on lines +744 to +793
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

restoreFromCloudsaveAllbackupToCloud creates a wasteful round-trip.

Line 786 calls saveAll(), which (line 64) immediately calls backupToCloud(false), pushing the just-restored data right back to Firestore. This is a redundant write on every restore and could also cause unexpected timestamp advancement.

Consider splitting the persistence concern: save to local storage without triggering cloud backup after a restore.

🔧 Proposed approach — add a flag to skip cloud backup
-function saveAll() {
+function saveAll({ skipCloudBackup = false } = {}) {
   lastUpdatedAt = Date.now();
   chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt });
-  backupToCloud(false);
+  if (!skipCloudBackup) backupToCloud(false);
 }

Then in restoreFromCloud:

-    saveAll();
+    saveAll({ skipCloudBackup: true });
🤖 Prompt for AI Agents
In `@script.js` around lines 744 - 793, restoreFromCloud currently calls saveAll()
which immediately triggers backupToCloud and writes the restored data back to
Firestore; stop that by adding a way to save locally without invoking cloud
backup: modify saveAll to accept an optional flag (e.g., saveAll(skipCloud =
false)) or create a saveLocalOnly wrapper that performs the same local
persistence steps but does not call backupToCloud, update saveAll implementation
to call backupToCloud only when skipCloud is false, and change restoreFromCloud
to call saveAll(true) (or saveLocalOnly) after populating
bookmarks/folders/openFolders; ensure other callers of saveAll keep the default
behavior so normal saves still back up to cloud.

}
44 changes: 44 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,47 @@ body::before {
height: 60px;
}
}

/* ===================================
FIREBASE CLOUD PANEL
=================================== */
.cloud-panel {
margin-top: 16px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 12px;
}

.cloud-panel h3 {
margin-bottom: 10px;
font-size: 14px;
color: #bfdbfe;
}

.cloud-panel input {
width: 100%;
padding: 10px;
margin-bottom: 8px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(15, 23, 42, 0.6);
color: #e2e8f0;
}

.cloud-actions {
display: grid;
gap: 8px;
grid-template-columns: 1fr;
}

.cloud-actions .small-btn {
margin-bottom: 0;
}

.cloud-status {
margin-top: 10px;
font-size: 12px;
color: #93c5fd;
line-height: 1.4;
}