mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
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:
@@ -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
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user