mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
feat(security): improvce IPC event handling and WebView security settings to prevent unauthorized access
This commit is contained in:
parent
0f62f8107b
commit
879075cb15
10 changed files with 264 additions and 40 deletions
|
|
@ -214,9 +214,11 @@ public class MainActivity extends AppCompatActivity {
|
|||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
webSettings.setDatabaseEnabled(true);
|
||||
webSettings.setAllowFileAccess(true);
|
||||
webSettings.setAllowFileAccess(false);
|
||||
webSettings.setAllowContentAccess(true);
|
||||
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
webSettings.setAllowFileAccessFromFileURLs(false);
|
||||
webSettings.setAllowUniversalAccessFromFileURLs(false);
|
||||
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
|
||||
webSettings.setMediaPlaybackRequiresUserGesture(false);
|
||||
webSettings.setGeolocationEnabled(true);
|
||||
|
||||
|
|
@ -2031,4 +2033,3 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const {
|
|||
isLocalBackendUrl,
|
||||
shouldOpenInElectronWindow,
|
||||
shouldAllowInWindowNavigation,
|
||||
isTrustedIpcEvent,
|
||||
} = require("./mainHelpers");
|
||||
const { isAllowedShellPath } = require("./shellPathGuard");
|
||||
const { normalizeExternalUrlForOpen } = require("./safeExternalUrl");
|
||||
|
|
@ -156,18 +157,27 @@ app.on("open-url", (event, url) => {
|
|||
}
|
||||
});
|
||||
|
||||
function trustedIpcHandle(channel, listener) {
|
||||
ipcMain.handle(channel, async (event, ...args) => {
|
||||
if (!isTrustedIpcEvent(event)) {
|
||||
throw new Error("MeshChatX IPC blocked for this origin");
|
||||
}
|
||||
return listener(event, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
// allow fetching app version via ipc
|
||||
ipcMain.handle("app-version", () => {
|
||||
trustedIpcHandle("app-version", () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
// allow fetching hardware acceleration status via ipc
|
||||
ipcMain.handle("is-hardware-acceleration-enabled", () => {
|
||||
trustedIpcHandle("is-hardware-acceleration-enabled", () => {
|
||||
return app.isHardwareAccelerationEnabled();
|
||||
});
|
||||
|
||||
// allow fetching integrity status
|
||||
ipcMain.handle("get-integrity-status", () => {
|
||||
trustedIpcHandle("get-integrity-status", () => {
|
||||
return integrityStatus;
|
||||
});
|
||||
|
||||
|
|
@ -179,7 +189,7 @@ const {
|
|||
closeAllMessageNotifications,
|
||||
} = require("./messageNotifications.js");
|
||||
|
||||
ipcMain.handle("show-notification", (event, { title, body, silent, destinationHash }) => {
|
||||
trustedIpcHandle("show-notification", (event, { title, body, silent, destinationHash }) => {
|
||||
const notification = new Notification({
|
||||
title: title,
|
||||
body: body,
|
||||
|
|
@ -199,7 +209,7 @@ ipcMain.handle("show-notification", (event, { title, body, silent, destinationHa
|
|||
});
|
||||
});
|
||||
|
||||
ipcMain.handle("close-message-notifications", (_event, destinationHash) => {
|
||||
trustedIpcHandle("close-message-notifications", (_event, destinationHash) => {
|
||||
if (destinationHash) {
|
||||
return closeMessageNotificationsFor(destinationHash);
|
||||
}
|
||||
|
|
@ -207,7 +217,7 @@ ipcMain.handle("close-message-notifications", (_event, destinationHash) => {
|
|||
});
|
||||
|
||||
// Power Management IPC
|
||||
ipcMain.handle("set-power-save-blocker", (event, enabled) => {
|
||||
trustedIpcHandle("set-power-save-blocker", (event, enabled) => {
|
||||
if (enabled) {
|
||||
if (activePowerSaveBlockerId === null) {
|
||||
activePowerSaveBlockerId = powerSaveBlocker.start("prevent-app-suspension");
|
||||
|
|
@ -226,28 +236,28 @@ ipcMain.handle("set-power-save-blocker", (event, enabled) => {
|
|||
// ignore ssl errors
|
||||
app.commandLine.appendSwitch("ignore-certificate-errors");
|
||||
|
||||
ipcMain.handle("backend-http-only", () => {
|
||||
trustedIpcHandle("backend-http-only", () => {
|
||||
return getUserProvidedArguments(process.argv).includes("--no-https");
|
||||
});
|
||||
|
||||
ipcMain.handle("backend-runtime-state", () => {
|
||||
trustedIpcHandle("backend-runtime-state", () => {
|
||||
return getBackendManager().getRuntimeState();
|
||||
});
|
||||
|
||||
ipcMain.handle("backend-startup-diagnostics", () => {
|
||||
trustedIpcHandle("backend-startup-diagnostics", () => {
|
||||
return getBackendManager().getStartupDiagnostics();
|
||||
});
|
||||
|
||||
ipcMain.handle("mark-backend-healthy", () => {
|
||||
trustedIpcHandle("mark-backend-healthy", () => {
|
||||
getBackendManager().markBackendHealthy();
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
ipcMain.handle("restart-backend", async () => {
|
||||
trustedIpcHandle("restart-backend", async () => {
|
||||
return await getBackendManager().restartBackend(integrityStatus);
|
||||
});
|
||||
|
||||
ipcMain.handle("open-backend-crash-report", async () => {
|
||||
trustedIpcHandle("open-backend-crash-report", async () => {
|
||||
const lastCrash = getBackendManager().getLastCrash();
|
||||
if (!lastCrash) {
|
||||
return { ok: false, error: "No backend crash report is available." };
|
||||
|
|
@ -256,7 +266,7 @@ ipcMain.handle("open-backend-crash-report", async () => {
|
|||
return { ok: true };
|
||||
});
|
||||
|
||||
ipcMain.handle("crash-recovery-info", () => {
|
||||
trustedIpcHandle("crash-recovery-info", () => {
|
||||
const manager = getBackendManager();
|
||||
const lastCrash = manager.getLastCrash() || {};
|
||||
const logs = manager.getJoinedLogs();
|
||||
|
|
@ -271,7 +281,7 @@ ipcMain.handle("crash-recovery-info", () => {
|
|||
});
|
||||
});
|
||||
|
||||
ipcMain.handle("restore-database-backup", async (_event, backupPath) => {
|
||||
trustedIpcHandle("restore-database-backup", async (_event, backupPath) => {
|
||||
if (!backupPath || typeof backupPath !== "string") {
|
||||
return { ok: false, error: "No backup path provided." };
|
||||
}
|
||||
|
|
@ -291,7 +301,7 @@ ipcMain.handle("restore-database-backup", async (_event, backupPath) => {
|
|||
return await getBackendManager().runMaintenanceTask(["--restore-db", backupPath]);
|
||||
});
|
||||
|
||||
ipcMain.handle("pick-database-backup", async () => {
|
||||
trustedIpcHandle("pick-database-backup", async () => {
|
||||
const win = getDialogParentWindow();
|
||||
if (!win) {
|
||||
return null;
|
||||
|
|
@ -307,14 +317,14 @@ ipcMain.handle("pick-database-backup", async () => {
|
|||
});
|
||||
|
||||
// add support for showing an alert window via ipc
|
||||
ipcMain.handle("alert", async (event, message) => {
|
||||
trustedIpcHandle("alert", async (event, message) => {
|
||||
return await dialog.showMessageBox(mainWindow, {
|
||||
message: message,
|
||||
});
|
||||
});
|
||||
|
||||
// add support for showing a confirm window via ipc
|
||||
ipcMain.handle("confirm", async (event, message) => {
|
||||
trustedIpcHandle("confirm", async (event, message) => {
|
||||
// show confirm dialog
|
||||
const result = await dialog.showMessageBox(mainWindow, {
|
||||
type: "question",
|
||||
|
|
@ -333,7 +343,7 @@ ipcMain.handle("confirm", async (event, message) => {
|
|||
});
|
||||
|
||||
// add support for showing a prompt window via ipc
|
||||
ipcMain.handle("prompt", async (event, message, defaultValue = "") => {
|
||||
trustedIpcHandle("prompt", async (event, message, defaultValue = "") => {
|
||||
return await electronPrompt({
|
||||
title: message,
|
||||
label: "",
|
||||
|
|
@ -346,7 +356,7 @@ ipcMain.handle("prompt", async (event, message, defaultValue = "") => {
|
|||
});
|
||||
|
||||
// allow relaunching app via ipc
|
||||
ipcMain.handle("relaunch", () => {
|
||||
trustedIpcHandle("relaunch", () => {
|
||||
const relaunchOptions = {};
|
||||
if (!process.defaultApp && process.platform === "linux" && process.env.APPIMAGE) {
|
||||
relaunchOptions.execPath = process.env.APPIMAGE;
|
||||
|
|
@ -356,7 +366,7 @@ ipcMain.handle("relaunch", () => {
|
|||
quit();
|
||||
});
|
||||
|
||||
ipcMain.handle("relaunch-emergency", () => {
|
||||
trustedIpcHandle("relaunch-emergency", () => {
|
||||
const relaunchOptions = {
|
||||
args: process.argv.slice(1).concat(["--emergency"]),
|
||||
};
|
||||
|
|
@ -368,7 +378,7 @@ ipcMain.handle("relaunch-emergency", () => {
|
|||
quit();
|
||||
});
|
||||
|
||||
ipcMain.handle("relaunch-auto-recover", () => {
|
||||
trustedIpcHandle("relaunch-auto-recover", () => {
|
||||
const relaunchOptions = {
|
||||
args: process.argv.slice(1).concat(["--auto-recover"]),
|
||||
};
|
||||
|
|
@ -380,32 +390,32 @@ ipcMain.handle("relaunch-auto-recover", () => {
|
|||
quit();
|
||||
});
|
||||
|
||||
ipcMain.handle("shutdown", () => {
|
||||
trustedIpcHandle("shutdown", () => {
|
||||
isQuiting = true;
|
||||
quit();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-close-settings", () => {
|
||||
trustedIpcHandle("get-close-settings", () => {
|
||||
return getCloseSettings();
|
||||
});
|
||||
|
||||
ipcMain.handle("set-close-settings", (_event, partial) => {
|
||||
trustedIpcHandle("set-close-settings", (_event, partial) => {
|
||||
return updateCloseSettings(partial || {});
|
||||
});
|
||||
|
||||
ipcMain.handle("get-screen-security-settings", () => {
|
||||
trustedIpcHandle("get-screen-security-settings", () => {
|
||||
return getScreenSecuritySettingsPayload();
|
||||
});
|
||||
|
||||
ipcMain.handle("set-screen-security-enabled", (_event, enabled) => {
|
||||
trustedIpcHandle("set-screen-security-enabled", (_event, enabled) => {
|
||||
return updateScreenSecurityEnabled(enabled === true);
|
||||
});
|
||||
|
||||
ipcMain.handle("get-memory-usage", async () => {
|
||||
trustedIpcHandle("get-memory-usage", async () => {
|
||||
return process.getProcessMemoryInfo();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-battery-status", async () => {
|
||||
trustedIpcHandle("get-battery-status", async () => {
|
||||
let onBattery = null;
|
||||
try {
|
||||
if (typeof powerMonitor?.isOnBatteryPower === "function") {
|
||||
|
|
@ -455,7 +465,7 @@ ipcMain.handle("get-battery-status", async () => {
|
|||
});
|
||||
|
||||
// allow showing a file path in os file manager
|
||||
ipcMain.handle("showPathInFolder", (event, targetPath) => {
|
||||
trustedIpcHandle("showPathInFolder", (event, targetPath) => {
|
||||
const ctx = {
|
||||
app,
|
||||
getDefaultStorageDir,
|
||||
|
|
@ -469,7 +479,7 @@ ipcMain.handle("showPathInFolder", (event, targetPath) => {
|
|||
shell.showItemInFolder(targetPath);
|
||||
});
|
||||
|
||||
ipcMain.handle("open-path", (event, targetPath) => {
|
||||
trustedIpcHandle("open-path", (event, targetPath) => {
|
||||
const ctx = {
|
||||
app,
|
||||
getDefaultStorageDir,
|
||||
|
|
@ -483,7 +493,7 @@ ipcMain.handle("open-path", (event, targetPath) => {
|
|||
return shell.openPath(targetPath);
|
||||
});
|
||||
|
||||
ipcMain.handle("pick-file", async () => {
|
||||
trustedIpcHandle("pick-file", async () => {
|
||||
const win = getDialogParentWindow();
|
||||
if (!win) {
|
||||
return null;
|
||||
|
|
@ -497,7 +507,7 @@ ipcMain.handle("pick-file", async () => {
|
|||
return filePaths[0];
|
||||
});
|
||||
|
||||
ipcMain.handle("pick-directory", async () => {
|
||||
trustedIpcHandle("pick-directory", async () => {
|
||||
const win = getDialogParentWindow();
|
||||
if (!win) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@ const {
|
|||
isTrustedBlobUrl,
|
||||
isTrustedShellFileUrl,
|
||||
isTrustedShellOrigin,
|
||||
isTrustedIpcEvent,
|
||||
senderUrlFromIpcEvent,
|
||||
shouldOpenInElectronWindow,
|
||||
shouldAllowInWindowNavigation,
|
||||
} = require("./shellOrigin");
|
||||
|
|
@ -136,6 +138,8 @@ module.exports = {
|
|||
isTrustedBlobUrl,
|
||||
isTrustedShellFileUrl,
|
||||
isTrustedShellOrigin,
|
||||
isTrustedIpcEvent,
|
||||
senderUrlFromIpcEvent,
|
||||
shouldOpenInElectronWindow,
|
||||
shouldAllowInWindowNavigation,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -154,11 +154,48 @@ function shouldAllowInWindowNavigation(url) {
|
|||
return isLocalBackendUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL of the renderer frame that invoked an ipcMain handler.
|
||||
* Prefers senderFrame.url, then sender.getURL().
|
||||
* @param {unknown} event
|
||||
* @returns {string}
|
||||
*/
|
||||
function senderUrlFromIpcEvent(event) {
|
||||
if (!event || typeof event !== "object") {
|
||||
return "";
|
||||
}
|
||||
const frame = event.senderFrame;
|
||||
if (frame && typeof frame.url === "string" && frame.url) {
|
||||
return frame.url;
|
||||
}
|
||||
const sender = event.sender;
|
||||
if (sender && typeof sender.getURL === "function") {
|
||||
try {
|
||||
const url = sender.getURL();
|
||||
return typeof url === "string" ? url : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether ipcMain may run for this invoke. Same allowlist as preload.
|
||||
* @param {unknown} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTrustedIpcEvent(event) {
|
||||
return isTrustedShellOrigin(senderUrlFromIpcEvent(event));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isLocalBackendUrl,
|
||||
isTrustedBlobUrl,
|
||||
isTrustedShellFileUrl,
|
||||
isTrustedShellOrigin,
|
||||
isTrustedIpcEvent,
|
||||
senderUrlFromIpcEvent,
|
||||
shouldOpenInElectronWindow,
|
||||
shouldAllowInWindowNavigation,
|
||||
};
|
||||
|
|
|
|||
BIN
meshchatx.rsm
BIN
meshchatx.rsm
Binary file not shown.
|
|
@ -8,6 +8,7 @@ import json
|
|||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
KNOWN_HOOKS = frozenset(
|
||||
{
|
||||
|
|
@ -42,12 +43,20 @@ KNOWN_MANAGERS = frozenset(
|
|||
KNOWN_STORAGE = frozenset({"isolated", "none"})
|
||||
KNOWN_NETWORK = frozenset({"none", "fetch"})
|
||||
|
||||
_URL_IN_TEXT_RE = re.compile(r"""https?://[^\s"'<>\\)\]]+""")
|
||||
_URL_IN_TEXT_RE = re.compile(r"""https?://[^\s"'<>\\)]+""")
|
||||
_SCHEME_HOST_RE = re.compile(
|
||||
r"https?://([a-z0-9][-a-z0-9.]*(?:\.[a-z0-9][-a-z0-9.]*)+)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_SCAN_EXTENSIONS = frozenset({".js", ".mjs", ".json", ".wasm", ".ts", ".go", ".wat"})
|
||||
_LOOPBACK_OR_UNSPECIFIED_HOSTS = frozenset(
|
||||
{
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"::1",
|
||||
"0.0.0.0",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def permission_id_for_hook(hook: str) -> str:
|
||||
|
|
@ -183,13 +192,25 @@ def _is_http_url(value: str) -> bool:
|
|||
return lower.startswith("http://") or lower.startswith("https://")
|
||||
|
||||
|
||||
def _hostname_is_loopback_or_unspecified(hostname: str | None) -> bool:
|
||||
if not hostname:
|
||||
return False
|
||||
host = hostname.strip().lower().strip("[]")
|
||||
return host in _LOOPBACK_OR_UNSPECIFIED_HOSTS
|
||||
|
||||
|
||||
def _is_external_http_url(value: str) -> bool:
|
||||
if not _is_http_url(value):
|
||||
return False
|
||||
lower = value.lower()
|
||||
if "localhost" in lower or "127.0.0.1" in lower or "0.0.0.0" in lower:
|
||||
return False
|
||||
if "/_plugins/" in lower or "/api/v1/plugins/" in lower:
|
||||
try:
|
||||
parsed = urlparse(value)
|
||||
hostname = parsed.hostname
|
||||
except (ValueError, UnicodeError):
|
||||
return True
|
||||
scheme = (parsed.scheme or "").lower()
|
||||
if scheme not in ("http", "https"):
|
||||
return True
|
||||
if _hostname_is_loopback_or_unspecified(hostname):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,18 @@ def test_extract_and_collect_network_endpoints(tmp_path):
|
|||
text = 'const url = "https://api.example.com/v1"; fetch("http://localhost/ignore");'
|
||||
assert extract_urls_from_text(text) == ["https://api.example.com/v1"]
|
||||
|
||||
query = 'fetch("https://api.example.com/?x=127.0.0.1")'
|
||||
assert extract_urls_from_text(query) == ["https://api.example.com/?x=127.0.0.1"]
|
||||
userinfo = 'fetch("http://127.0.0.1:9337@example.com/v1")'
|
||||
assert extract_urls_from_text(userinfo) == ["http://127.0.0.1:9337@example.com/v1"]
|
||||
substring_host = 'fetch("https://notlocalhost.com/a"); fetch("https://127.0.0.1.example.com/")'
|
||||
extracted = extract_urls_from_text(substring_host)
|
||||
assert "https://notlocalhost.com/a" in extracted
|
||||
assert "https://127.0.0.1.example.com/" in extracted
|
||||
assert extract_urls_from_text('fetch("http://localhost/ignore")') == []
|
||||
assert extract_urls_from_text('fetch("http://127.0.0.1:9337/api/v1/plugins/x")') == []
|
||||
assert extract_urls_from_text('fetch("http://[::1]:8000/")') == []
|
||||
|
||||
plugin_dir = tmp_path / "plugin"
|
||||
plugin_dir.mkdir()
|
||||
(plugin_dir / "frontend").mkdir()
|
||||
|
|
|
|||
116
tests/backend/test_ws_origin_filesync_oracles.py
Normal file
116
tests/backend/test_ws_origin_filesync_oracles.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Security oracles for cross-origin WebSocket access and the FileSync sync-root jail.
|
||||
|
||||
Invariants under test:
|
||||
|
||||
1. A WebSocket upgrade whose Origin is not the local backend origin must be
|
||||
rejected, regardless of whether HTTP auth is enabled.
|
||||
2. A cross-origin WebSocket client with no session must not mutate server state.
|
||||
3. The FileSync sync directory must never resolve to a sensitive identity-tree
|
||||
directory such as ssl (TLS key material), while ordinary subdirectories
|
||||
remain selectable.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from aiohttp import WSServerHandshakeError, WSMsgType
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
|
||||
from meshchatx.src.backend.rns_filesync_handler import RnsFilesyncHandler
|
||||
from tests.backend.test_http_auth_security import _make_aio_app
|
||||
|
||||
EVIL_ORIGIN = "https://evil.example"
|
||||
|
||||
|
||||
def _patch_ws_broadcasts(mock_app):
|
||||
mock_app.send_config_to_websocket_clients = AsyncMock()
|
||||
mock_app.send_active_sessions_to_websocket_clients = AsyncMock()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("require_loopback_tcp")
|
||||
async def test_ws_upgrade_rejects_cross_site_origin(mock_app):
|
||||
"""Invariant 1: hostile Origin must not complete the /ws upgrade."""
|
||||
_patch_ws_broadcasts(mock_app)
|
||||
aio_app = _make_aio_app(mock_app, use_https=False)
|
||||
|
||||
async with TestClient(TestServer(aio_app)) as client:
|
||||
with pytest.raises(WSServerHandshakeError):
|
||||
await client.ws_connect("/ws", origin=EVIL_ORIGIN)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("require_loopback_tcp")
|
||||
async def test_cross_origin_ws_mutator_does_not_change_state(mock_app):
|
||||
"""Invariant 2: no session and hostile Origin means no state mutation."""
|
||||
_patch_ws_broadcasts(mock_app)
|
||||
aio_app = _make_aio_app(mock_app, use_https=False)
|
||||
|
||||
async with TestClient(TestServer(aio_app)) as client:
|
||||
try:
|
||||
ws = await client.ws_connect("/ws", origin=EVIL_ORIGIN)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
probe_action = "oracle_cross_origin_probe"
|
||||
await ws.send_str(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "keyboard_shortcuts.set",
|
||||
"action": probe_action,
|
||||
"keys": ["ctrl+alt+9"],
|
||||
},
|
||||
),
|
||||
)
|
||||
try:
|
||||
while True:
|
||||
msg = await ws.receive(timeout=2)
|
||||
if msg.type in (WSMsgType.CLOSED, WSMsgType.ERROR):
|
||||
break
|
||||
if msg.type == WSMsgType.TEXT and "keyboard_shortcuts" in msg.data:
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
stored = mock_app.database.misc.get_keyboard_shortcuts(
|
||||
mock_app.identity.hash.hex(),
|
||||
)
|
||||
assert all(s["action"] != probe_action for s in stored)
|
||||
|
||||
|
||||
def _make_filesync_handler(tmp_path):
|
||||
storage = tmp_path / "identity-storage"
|
||||
storage.mkdir()
|
||||
return RnsFilesyncHandler(
|
||||
reticulum_instance=MagicMock(),
|
||||
identity=MagicMock(),
|
||||
storage_dir=str(storage),
|
||||
), storage
|
||||
|
||||
|
||||
def test_filesync_sync_dir_rejects_identity_ssl_dir(tmp_path):
|
||||
"""Invariant 3: ssl holds the identity TLS key and must not be syncable."""
|
||||
handler, storage = _make_filesync_handler(tmp_path)
|
||||
target = os.path.join(str(storage), "ssl")
|
||||
os.makedirs(target, exist_ok=True)
|
||||
assert handler._resolve_sync_directory(target) is None
|
||||
|
||||
|
||||
def test_filesync_sync_dir_rejects_identity_root_and_escape(tmp_path):
|
||||
"""Controls: identity root and sibling escape stay rejected today."""
|
||||
handler, storage = _make_filesync_handler(tmp_path)
|
||||
assert handler._resolve_sync_directory(str(storage)) is None
|
||||
sibling = os.path.join(str(storage), "..", "other-identity")
|
||||
assert handler._resolve_sync_directory(sibling) is None
|
||||
|
||||
|
||||
def test_filesync_sync_dir_allows_plain_subdirectory(tmp_path):
|
||||
"""Control: an ordinary identity-storage subdirectory stays selectable."""
|
||||
handler, storage = _make_filesync_handler(tmp_path)
|
||||
target = os.path.join(str(storage), "shared-docs")
|
||||
os.makedirs(target, exist_ok=True)
|
||||
assert handler._resolve_sync_directory(target) is not None
|
||||
|
|
@ -86,6 +86,20 @@ describe("electron/mainHelpers", () => {
|
|||
expect(isTrustedShellOrigin("file:///tmp/evil.html")).toBe(false);
|
||||
});
|
||||
|
||||
it("isTrustedIpcEvent uses senderFrame.url then sender.getURL", () => {
|
||||
const { isTrustedIpcEvent } = require("../../electron/mainHelpers.js");
|
||||
expect(isTrustedIpcEvent({ senderFrame: { url: "https://127.0.0.1:9337/" } })).toBe(true);
|
||||
expect(isTrustedIpcEvent({ senderFrame: { url: "http://127.0.0.1:9337@example.com/" } })).toBe(false);
|
||||
expect(isTrustedIpcEvent({ senderFrame: { url: "https://example.com/" } })).toBe(false);
|
||||
expect(
|
||||
isTrustedIpcEvent({
|
||||
sender: { getURL: () => "file:///opt/meshchatx/electron/loading.html" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(isTrustedIpcEvent({})).toBe(false);
|
||||
expect(isTrustedIpcEvent(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("parseArgvFlag reads a value following the flag", () => {
|
||||
expect(parseArgvFlag(["--storage-dir", "/mnt/persist"], "--storage-dir")).toBe("/mnt/persist");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -182,12 +182,21 @@ describe("behavior contracts: user-visible wiring must stay connected", () => {
|
|||
const origin = readSource("electron/shellOrigin.js");
|
||||
expect(origin).not.toMatch(/startsWith\(\s*["']https?:\/\/127/);
|
||||
expect(origin).toContain("parsed.username");
|
||||
expect(origin).toContain("isTrustedIpcEvent");
|
||||
expect(main).toContain("function trustedIpcHandle");
|
||||
expect(main).toContain("isTrustedIpcEvent");
|
||||
expect(main).not.toMatch(/ipcMain\.handle\("/);
|
||||
});
|
||||
|
||||
it("Android WebView opens external http(s) in the system browser", () => {
|
||||
const src = readSource("android/app/src/main/java/com/meshchatx/MainActivity.java");
|
||||
expect(src).toContain("openExternalBrowserUri");
|
||||
expect(src).toContain("Intent.ACTION_VIEW");
|
||||
expect(src).toContain("setAllowFileAccess(false)");
|
||||
expect(src).toContain("setAllowFileAccessFromFileURLs(false)");
|
||||
expect(src).toContain("setAllowUniversalAccessFromFileURLs(false)");
|
||||
expect(src).toContain("MIXED_CONTENT_NEVER_ALLOW");
|
||||
expect(src).not.toContain("MIXED_CONTENT_ALWAYS_ALLOW");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue