完善桌面端回归与安全边界复审
This commit is contained in:
@@ -86,7 +86,21 @@ const verifyCapabilities = async () => {
|
||||
"fs:allow-read-dir",
|
||||
"fs:allow-read-text-file",
|
||||
"fs:allow-write-text-file",
|
||||
"notification:default",
|
||||
"opener:default",
|
||||
"opener:allow-open-url",
|
||||
"opener:allow-reveal-item-in-dir",
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download",
|
||||
"updater:allow-install",
|
||||
"updater:allow-download-and-install",
|
||||
]);
|
||||
const requiredNotificationPermissions = [
|
||||
"notification:allow-is-permission-granted",
|
||||
"notification:allow-request-permission",
|
||||
"notification:allow-notify",
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
const capability = await readJson(resolve(capabilitiesDir, file));
|
||||
@@ -97,6 +111,19 @@ const verifyCapabilities = async () => {
|
||||
assert(!identifier.startsWith("shell:"), `${file}: shell permissions are not allowed.`);
|
||||
assert(!bannedPermissions.has(identifier), `${file}: ${identifier} is not allowed for CTMS Desktop.`);
|
||||
assert(!identifier.includes("persisted-scope"), `${file}: persisted filesystem scopes are not allowed.`);
|
||||
assert(!identifier.startsWith("updater:"), `${file}: updater permissions must not be exposed directly to WebView.`);
|
||||
if (identifier.startsWith("notification:")) {
|
||||
assert(
|
||||
requiredNotificationPermissions.includes(identifier),
|
||||
`${file}: notification permission ${identifier} is broader than the CTMS Desktop notification boundary.`,
|
||||
);
|
||||
}
|
||||
if (identifier.startsWith("opener:")) {
|
||||
assert(identifier === "opener:allow-open-path", `${file}: opener permission ${identifier} is not allowed.`);
|
||||
}
|
||||
}
|
||||
for (const identifier of requiredNotificationPermissions) {
|
||||
assert(identifiers.includes(identifier), `${file}: missing ${identifier}.`);
|
||||
}
|
||||
|
||||
const fsScope = permissions.find((permission) => permissionIdentifier(permission) === "fs:scope");
|
||||
@@ -127,6 +154,19 @@ const verifyRustBoundary = async () => {
|
||||
dialogIndex < 0 || singleInstanceIndex < dialogIndex,
|
||||
"Single-instance plugin must be registered before other desktop plugins.",
|
||||
);
|
||||
const singleInstanceSource = libSource.slice(
|
||||
singleInstanceIndex,
|
||||
dialogIndex > singleInstanceIndex ? dialogIndex : singleInstanceIndex + 600,
|
||||
);
|
||||
const restoreWindowTokens = ['get_webview_window("main")', "window.unminimize()", "window.show()", "window.set_focus()"];
|
||||
for (const token of restoreWindowTokens) {
|
||||
assert(singleInstanceSource.includes(token), `Single-instance duplicate launch handler must call ${token}.`);
|
||||
}
|
||||
assert(
|
||||
singleInstanceSource.indexOf("window.unminimize()") < singleInstanceSource.indexOf("window.show()") &&
|
||||
singleInstanceSource.indexOf("window.show()") < singleInstanceSource.indexOf("window.set_focus()"),
|
||||
"Single-instance duplicate launch handler must restore, show, then focus the main window.",
|
||||
);
|
||||
|
||||
const handlerSource = libSource.match(/generate_handler!\s*\\?\[([\s\S]*?)\]/)?.[1] || "";
|
||||
const commands = handlerSource.match(/[a-z_]+::[a-z_]+/g) || [];
|
||||
@@ -153,9 +193,9 @@ const verifySourceSafety = async () => {
|
||||
for (const path of files) {
|
||||
const source = await readFile(path, "utf8");
|
||||
const file = relative(rootDir, path);
|
||||
assert(!/[?&]token=/.test(source), `${file}: token must not be passed through query parameters.`);
|
||||
assert(!/[?&](?:token|access_token)=/i.test(source), `${file}: token must not be passed through query parameters.`);
|
||||
assert(
|
||||
!/console\.(log|debug|info|warn|error)\s*\([^)]*token/i.test(source),
|
||||
!/console\.(log|debug|info|warn|error)\s*\([^)]*(?:token|access_token|authorization|bearer)/i.test(source),
|
||||
`${file}: token-related values must not be written to console logs.`,
|
||||
);
|
||||
if (source.includes("ctms_token") && file !== "frontend/src/runtime/secureSessionStorage.ts") {
|
||||
@@ -174,6 +214,19 @@ const verifyNotificationBoundary = async () => {
|
||||
assert(!/showSystemNotification\s*=\s*async\s*\([^)]*[a-zA-Z]/.test(source), "Desktop notification body must not accept dynamic business content.");
|
||||
};
|
||||
|
||||
const verifySessionBoundary = async () => {
|
||||
const source = await readFile(resolve(sourceDir, "session/sessionManager.ts"), "utf8");
|
||||
const tokenBroadcastIndex = source.indexOf('message.type === "TOKEN_UPDATED"');
|
||||
const storageBroadcastIndex = source.indexOf('localStorage.setItem("ctms_auth_broadcast"');
|
||||
|
||||
assert(tokenBroadcastIndex >= 0, "Session manager must branch TOKEN_UPDATED broadcasts before storage fallback.");
|
||||
assert(storageBroadcastIndex >= 0, "Session manager must keep storage fallback for non-token auth broadcasts.");
|
||||
assert(
|
||||
tokenBroadcastIndex >= 0 && storageBroadcastIndex >= 0 && tokenBroadcastIndex < storageBroadcastIndex,
|
||||
"Session manager must not persist TOKEN_UPDATED payloads through localStorage broadcast fallback.",
|
||||
);
|
||||
};
|
||||
|
||||
const verifyUpdaterBoundary = async () => {
|
||||
const source = await readFile(resolve(tauriDir, "src/updates.rs"), "utf8");
|
||||
assert(source.includes('join("desktop-updates/stable/latest.json")'), "Desktop updater must derive the fixed stable latest.json path.");
|
||||
@@ -238,6 +291,7 @@ await verifyCapabilities();
|
||||
await verifyRustBoundary();
|
||||
await verifySourceSafety();
|
||||
await verifyNotificationBoundary();
|
||||
await verifySessionBoundary();
|
||||
await verifyUpdaterBoundary();
|
||||
await verifyWorkflowGates();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user