fix: harden extension security and plugin reliability

Address audit findings across background handlers, openers,
plugins, and UI: URL allowlists, XSS reductions, popup lifecycle
fixes, plugin dispose/cleanup, cloud sync hardening, global search
mathjs sandbox, and settings storage fixes.
This commit is contained in:
2026-06-17 10:50:26 +09:30
parent 0e696e0175
commit 8a5424c5a4
70 changed files with 1229 additions and 430 deletions
+13 -5
View File
@@ -25,6 +25,11 @@ const REFRESH_URL = `${ACCOUNTS_BASE}/api/bsplus/refresh`;
const UPLOAD_DEBOUNCE_MS = 2000;
const POLL_THROTTLE_MS = 24 * 60 * 60 * 1000;
const POLL_THROTTLE_KEY = "bsplus_lastCloudPoll";
const FETCH_TIMEOUT_MS = 30_000;
function fetchWithTimeout(url: string, init?: RequestInit): Promise<Response> {
return fetch(url, { ...init, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
}
type CloudSummaryResponse = {
desqta?: unknown;
@@ -35,6 +40,7 @@ let reloadSeqtaPagesFn: (() => void) | null = null;
let suppressAutoUploadDuringRestore = false;
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
let pollInFlight: Promise<void> | null = null;
let autoSyncInitialized = false;
function isAutoCloudSyncEnabled(all: Record<string, unknown>): boolean {
return all.autoCloudSettingsSync !== false;
@@ -65,7 +71,7 @@ async function tryRefreshTokens(): Promise<boolean> {
if (!refresh_token || !client_id) return false;
try {
const r = await fetch(REFRESH_URL, {
const r = await fetchWithTimeout(REFRESH_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token, client_id }),
@@ -100,7 +106,7 @@ async function fetchCloudSummaryOnce(
| { ok: false; unauthorized: boolean; error?: string }
> {
try {
const r = await fetch(CLOUD_SUMMARY_URL, {
const r = await fetchWithTimeout(CLOUD_SUMMARY_URL, {
headers: { Authorization: `Bearer ${token}` },
cache: "no-store",
});
@@ -177,7 +183,7 @@ async function putSettingsOnce(token: string): Promise<PutResult> {
return { ok: true, skipped: true };
}
const r = await fetch(CLOUD_SETTINGS_SYNC_URL, {
const r = await fetchWithTimeout(CLOUD_SETTINGS_SYNC_URL, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
@@ -235,7 +241,7 @@ type GetResult =
async function getSettingsAndApplyOnce(token: string): Promise<GetResult> {
try {
const r = await fetch(CLOUD_SETTINGS_SYNC_URL, {
const r = await fetchWithTimeout(CLOUD_SETTINGS_SYNC_URL, {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
cache: "no-store",
@@ -373,8 +379,8 @@ export function runCloudSettingsPoll(): Promise<void> {
try {
const { [POLL_THROTTLE_KEY]: last } = await browser.storage.local.get(POLL_THROTTLE_KEY);
if (Date.now() - (Number(last) || 0) < POLL_THROTTLE_MS) return;
await browser.storage.local.set({ [POLL_THROTTLE_KEY]: Date.now() });
await runCloudSettingsPollInner();
await browser.storage.local.set({ [POLL_THROTTLE_KEY]: Date.now() });
} catch (e) {
console.error("[BS+ cloud sync] Poll error:", e);
} finally {
@@ -453,6 +459,8 @@ function onStorageChanged(
export function initCloudSettingsAutoSync(deps: { reloadSeqtaPages: () => void }): void {
reloadSeqtaPagesFn = deps.reloadSeqtaPages;
if (autoSyncInitialized) return;
autoSyncInitialized = true;
browser.storage.onChanged.addListener(onStorageChanged);
}
+20 -6
View File
@@ -1,5 +1,7 @@
import Parser from "rss-parser";
const MAX_RATE_LIMIT_RETRIES = 3;
/**
* Fetches news articles specifically for Australia from the NewsAPI.
*
@@ -13,15 +15,23 @@ import Parser from "rss-parser";
* to send the fetched news data back to the caller.
* It's called with an object like `{ news: responseData }`.
*/
const fetchAustraliaNews = async (url: string, sendResponse: any) => {
const fetchAustraliaNews = async (
url: string,
sendResponse: any,
rateLimitRetryCount = 0,
) => {
fetch(url)
.then((result) => result.json())
.then((response) => {
if (response.code == "rateLimited") {
fetchAustraliaNews((url += "%00"), sendResponse);
if (response.code == "rateLimited" && rateLimitRetryCount < MAX_RATE_LIMIT_RETRIES) {
fetchAustraliaNews(`${url}%00`, sendResponse, rateLimitRetryCount + 1);
} else {
sendResponse({ news: response });
}
})
.catch((error) => {
console.error("[BetterSEQTA+] Failed to fetch Australia news", error);
sendResponse({ news: { articles: [] } });
});
};
@@ -99,13 +109,14 @@ export async function fetchNews(source: string | undefined, sendResponse: any) {
if (normalizedSource === "australia") {
const date = new Date();
date.setDate(date.getDate() - 5);
const from =
date.getFullYear() +
"-" +
(date.getMonth() + 1) +
String(date.getMonth() + 1).padStart(2, "0") +
"-" +
(date.getDate() - 5);
String(date.getDate()).padStart(2, "0");
const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`;
fetchAustraliaNews(url, sendResponse);
@@ -115,7 +126,6 @@ export async function fetchNews(source: string | undefined, sendResponse: any) {
const parser = new Parser();
let feeds: string[];
console.log("fetchNews", normalizedSource);
if (rssFeedsByCountry[normalizedSource.toLowerCase()]) {
feeds = rssFeedsByCountry[normalizedSource.toLowerCase()];
@@ -129,6 +139,10 @@ export async function fetchNews(source: string | undefined, sendResponse: any) {
const articlesPromises = feeds.map(async (feedUrl) => {
try {
const response = await fetch(feedUrl);
if (!response.ok) {
console.error(`Failed to fetch RSS feed: ${feedUrl} (${response.status})`);
return [];
}
const feedString = await response.text();
const feed = await parser.parseString(feedString);