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
@@ -29,6 +29,9 @@ async function fetchJSON(url: string, body: any) {
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} for ${url}`);
}
return res.json();
}
@@ -164,7 +167,7 @@ async function getLearnAssessmentsData(studentId: number) {
}
export async function getAssessmentsData() {
if (settingsState.mockNotices) {
if (settingsState.hideSensitiveContent) {
return getMockAssessmentsData();
}
@@ -38,6 +38,9 @@ async function fetchJSON(url: string, body: unknown) {
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status} for ${url}`);
}
return res.json();
}
@@ -1,7 +1,7 @@
import type { Plugin } from "../../core/types";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { getAssessmentsData } from "./api";
import { renderErrorState, renderGrid, renderSkeletonLoader } from "./ui";
import { renderErrorState, renderGrid, renderSkeletonLoader, teardownOverviewUi } from "./ui";
import styles from "./styles.css?inline";
import { delay } from "@/seqta/utils/delay";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
@@ -66,6 +66,8 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
gridItem.appendChild(label);
menu.insertBefore(gridItem, menu.firstChild);
let loadRequestId = 0;
const menuObserver = new MutationObserver(() => {
ensureOverviewMenuPosition(menu, gridItem);
});
@@ -81,7 +83,18 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
};
gridItem.addEventListener("click", clickHandler);
const popstateHandler = () => {
if (isOverviewRoute()) {
void loadGridView();
} else {
loadRequestId += 1;
teardownOverviewUi();
}
};
window.addEventListener("popstate", popstateHandler);
async function loadGridView() {
const requestId = ++loadRequestId;
await delay(1);
if (isSeqtaEngageExperience()) {
@@ -98,7 +111,7 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
}
const main = document.getElementById("main");
if (!main) return;
if (!main || requestId !== loadRequestId) return;
document
.querySelectorAll('[data-key="assessments"] .item')
@@ -110,17 +123,22 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
.querySelector('[data-key="assessments"]')
?.classList.add("active");
main.innerHTML = '<div id="grid-view-container" class="bsplus-overview-host"></div>';
main.innerHTML =
'<div id="grid-view-container" class="bsplus-overview-host"></div>';
const container = document.getElementById(
"grid-view-container",
) as HTMLElement;
if (requestId !== loadRequestId) return;
renderSkeletonLoader(container);
try {
const data = await getAssessmentsData();
if (requestId !== loadRequestId) return;
renderGrid(container, data);
} catch (err) {
if (requestId !== loadRequestId) return;
console.error("Failed to load assessments:", err);
renderErrorState(
container,
@@ -130,8 +148,11 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
}
return () => {
loadRequestId += 1;
window.removeEventListener("popstate", popstateHandler);
menuObserver.disconnect();
gridItem.removeEventListener("click", clickHandler);
teardownOverviewUi();
gridItem.remove();
};
},
@@ -62,7 +62,7 @@ export function activeSubjectsFromEngageChild(child: {
const seen = new Set<string>();
for (const term of child.terms ?? []) {
if (term.active !== 1) continue;
if (!isActiveTermFlag(term.active)) continue;
for (const raw of term.subjects ?? []) {
const subject = normalizeOverviewSubject(raw);
if (!subject) continue;
@@ -151,7 +151,14 @@ export function determineStatus(item: any): string {
}
const completedKey = "betterseqta-completed-assessments";
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
let completed: unknown[] = [];
try {
const raw = localStorage.getItem(completedKey);
const parsed = raw ? JSON.parse(raw) : [];
completed = Array.isArray(parsed) ? parsed : [];
} catch {
completed = [];
}
if (completed.includes(item.id)) {
return "MARKS_RELEASED";
}