diff --git a/noname/game/index.js b/noname/game/index.js index 5a7cf3dac..597507be1 100644 --- a/noname/game/index.js +++ b/noname/game/index.js @@ -22,6 +22,8 @@ import { DynamicStyle } from "./dynamic-style/index.js"; import { GamePromises } from "./promises.js"; import { Check } from "./check.js"; +import security from "../util/security.js"; + export class Game { online = false; onlineID = null; @@ -1312,6 +1314,7 @@ export class Game { if (callback) callback(false); return; } + game.sandbox = security.createSandbox(); game.ws.onopen = lib.element.ws.onopen; game.ws.onmessage = lib.element.ws.onmessage; game.ws.onerror = lib.element.ws.onerror; @@ -2135,8 +2138,8 @@ export class Game { _status.importingExtension = true; try { // 导入普通扩展 - eval(str); - // esm扩展可以不写game.impoprt或许会导致_status.extensionLoading不存在 + security.eval(str); + // esm扩展可以不写game.import或许会导致_status.extensionLoading不存在 if (Array.isArray(_status.extensionLoading)) { await Promise.allSettled(_status.extensionLoading); delete _status.extensionLoading; @@ -5453,6 +5456,8 @@ export class Game { newvid.name1 = newvid.name2.slice(10, newvid.name1.lastIndexOf("_")); } lib.videos.unshift(newvid); + // 清洗代理对象 + newvid.video = structuredClone(newvid.video); store.put(newvid); ui.create.videoNode(newvid, true); } @@ -7695,7 +7700,8 @@ export class Game { ); lib.status.reload++; return new Promise((resolve, reject) => { - const record = lib.db.transaction([storeName], "readwrite").objectStore(storeName).put(value, idbValidKey); + const record = lib.db.transaction([storeName], "readwrite") + .objectStore(storeName).put(structuredClone(value), idbValidKey); record.onerror = event => { if (typeof onError == "function") { onError(event); diff --git a/noname/get/index.js b/noname/get/index.js index 98021f71f..f461cad68 100644 --- a/noname/get/index.js +++ b/noname/get/index.js @@ -1490,9 +1490,9 @@ export class Get { return Array.from(infos || []).map(get.infoPlayerOL); } /** 箭头函数头 */ - static #arrowPattern = /^(?:async\b\s*)?(?:([\w$]+)|\((\s*[\w$]+(?:\s*=\s*.+?)?(?:\s*,\s*[\w$]+(?:\s*=\s*.+?)?)*\s*)?\))\s*=>/; + #arrowPattern = /^(?:async\b\s*)?(?:([\w$]+)|\((\s*[\w$]+(?:\s*=\s*.+?)?(?:\s*,\s*[\w$]+(?:\s*=\s*.+?)?)*\s*)?\))\s*=>/; /** 标准函数头 */ - static #fullPattern = /^([\w\s*]+)\((\s*[\w$]+(?:\s*=\s*.+?)?(?:\s*,\s*[\w$]+(?:\s*=\s*.+?)?)*\s*)?\)\s*\{/; + #fullPattern = /^([\w\s*]+)\((\s*[\w$]+(?:\s*=\s*.+?)?(?:\s*,\s*[\w$]+(?:\s*=\s*.+?)?)*\s*)?\)\s*\{/; /** * ```plain * 测试一段代码是否为函数体 @@ -1501,7 +1501,7 @@ export class Get { * @param {string} code * @returns {boolean} */ - static isFunctionBody(code) { + isFunctionBody(code) { try { new Function(code); } catch (e) { @@ -1517,24 +1517,24 @@ export class Get { * @param {string} str * @returns */ - static pureFunctionStr(str) { + pureFunctionStr(str) { str = str.trim(); - const arrowMatch = Get.#arrowPattern.exec(str); + const arrowMatch = get.#arrowPattern.exec(str); if (arrowMatch) { const body = `return ${str.slice(arrowMatch[0].length)}`; - if (!Get.isFunctionBody(body)) { + if (!get.isFunctionBody(body)) { console.error("发现疑似恶意的远程代码:", str); return `()=>console.error("尝试执行疑似恶意的远程代码")`; } return `${arrowMatch[0]}{${body}}`; } if (!str.endsWith("}")) return '()=>console.warn("无法识别的远程代码")'; - const fullMatch = Get.#fullPattern.exec(str); + const fullMatch = get.#fullPattern.exec(str); if (!fullMatch) return '()=>console.warn("无法识别的远程代码")'; const head = fullMatch[1]; const args = fullMatch[2] || ''; const body = str.slice(fullMatch[0].length).slice(0, -1); - if (!Get.isFunctionBody(body)) { + if (!get.isFunctionBody(body)) { console.error("发现疑似恶意的远程代码:", str); return `()=>console.error("尝试执行疑似恶意的远程代码")`; } @@ -1552,13 +1552,13 @@ export class Get { const str = func.toString(); // js内置的函数 if (/\{\s*\[native code\]\s*\}/.test(str)) return "_noname_func:function () {}"; - return "_noname_func:" + Get.pureFunctionStr(str); + return "_noname_func:" + get.pureFunctionStr(str); } return ""; } infoFuncOL(info) { let func; - const str = Get.pureFunctionStr(info.slice(13)); // 清洗函数并阻止注入 + const str = get.pureFunctionStr(info.slice(13)); // 清洗函数并阻止注入 try { // js内置的函数 if (/\{\s*\[native code\]\s*\}/.test(str)) return function () { }; diff --git a/noname/init/index.js b/noname/init/index.js index ca396db2b..6c39036d3 100644 --- a/noname/init/index.js +++ b/noname/init/index.js @@ -10,6 +10,7 @@ import * as config from "../util/config.js"; import { promiseErrorHandlerMap } from "../util/browser.js"; import { importCardPack, importCharacterPack, importExtension, importMode } from "./import.js"; import { onload } from "./onload.js"; +import security from "../util/security.js"; // 判断是否从file协议切换到http/s协议 export function canUseHttpProtocol() { @@ -124,6 +125,11 @@ export async function boot() { // 加载polyfill内容 await import("./polyfill.js"); + // 初始化security + security.initSecurity({ + lib, game, ui, get, ai, _status, gnc, + }); + // 设定游戏加载时间,超过时间未加载就提醒 const configLoadTime = localStorage.getItem(lib.configprefix + "loadtime"); // 现在不暴露到全局变量里了,直接传给onload @@ -478,7 +484,8 @@ export async function boot() { //var backup_onload=lib.init.onload; _status.evaluatingExtension = true; try { - eval(extcontent); + debugger; // NEED TO VIEW DATA + security.eval(extcontent); } catch (e) { console.log(e); } diff --git a/noname/library/element/client.js b/noname/library/element/client.js index dadcc611f..279fa579b 100644 --- a/noname/library/element/client.js +++ b/noname/library/element/client.js @@ -3,6 +3,7 @@ import { game } from "../../game/index.js"; import { lib } from "../index.js"; import { _status } from "../../status/index.js"; import { ui } from "../../ui/index.js"; +import security from "../../util/security.js"; export class Client { /** @@ -17,6 +18,7 @@ export class Client { // @ts-ignore this.id = ws.wsid || get.id(); this.closed = false; + this.sandbox = security.createSandbox(); } send() { if (this.closed) return this; diff --git a/noname/library/element/gameEventPromise.js b/noname/library/element/gameEventPromise.js index 95089b042..b210c2021 100644 --- a/noname/library/element/gameEventPromise.js +++ b/noname/library/element/gameEventPromise.js @@ -3,6 +3,7 @@ import { game } from "../../game/index.js"; import { lib } from "../index.js"; import { _status } from "../../status/index.js"; import { AsyncFunction } from "../../util/index.js"; +import security from '../../util/security.js'; /** * 将事件Promise化以使用async异步函数来执行事件。 @@ -220,6 +221,7 @@ export class GameEventPromise extends Promise { * ``` */ async debugger() { + if (security.isSandboxRequired()) throw new Error("当前模式下禁止调试"); return new Promise((resolve) => { const runCode = function (event, code) { try { diff --git a/noname/library/element/player.js b/noname/library/element/player.js index ecd28d7b7..4ce81a277 100644 --- a/noname/library/element/player.js +++ b/noname/library/element/player.js @@ -20,6 +20,7 @@ import { _status } from "../../status/index.js"; import { ui } from "../../ui/index.js"; import { CacheContext } from "../cache/cacheContext.js"; import { ChildNodesWatcher } from "../cache/childNodesWatcher.js"; +import security from "../../util/security.js"; export class Player extends HTMLDivElement { /** @@ -454,7 +455,7 @@ export class Player extends HTMLDivElement { const vars = {}; /** * 作用域 - * @type { (code: string) => any } + * @type { ((code: string) => any)? } */ let scope; /** @type { Skill } */ @@ -500,14 +501,13 @@ export class Player extends HTMLDivElement { varstr += `var ${key}=lib.skill['${skillName}'].vars['${key}'];\n`; } let str = ` - function content(){ - ${varstr}if(event.triggername=='${skillName}After'){ - player.removeSkill('${skillName}'); - delete lib.skill['${skillName}']; - delete lib.translate['${skillName}']; - return event.finish(); - } - `; + ${varstr}if(event.triggername=='${skillName}After'){ + player.removeSkill('${skillName}'); + delete lib.skill['${skillName}']; + delete lib.translate['${skillName}']; + return event.finish(); + } + `; for (let i = 0; i < skill.contentFuns.length; i++) { const fun2 = skill.contentFuns[i]; const a = fun2.toString(); @@ -516,7 +516,15 @@ export class Player extends HTMLDivElement { const str2 = a.slice(begin, a.lastIndexOf("}") != -1 ? a.lastIndexOf("}") : undefined).trim(); str += `'step ${i}'\n\t${str2}\n\t`; } - skill.content = lib.init.parsex((scope || eval)(str + `\n};content;`), scope); + // 防止注入喵 + if (!get.isFunctionBody(str)) throw new Error("无效的content函数代码"); + let recompiledScope; + if (security.isSandboxRequired()) { + recompiledScope = scope ? security.eval(`return (${scope.toString()})`) : code => security.eval(`return (${code.toString()})`); + } else { + recompiledScope = scope || eval; + } + skill.content = lib.init.parsex(recompiledScope(`(function(){\n${str}\n})`), scope && recompiledScope); // @ts-ignore skill.content._parsed = true; }; @@ -632,6 +640,7 @@ export class Player extends HTMLDivElement { */ apply(_scope) { if (lib.skill[skillName] != skill) throw `This skill has been destroyed`; + if (security.isSandboxRequired()) console.warn("`player.when().apply()` 在沙盒模式下不推荐使用"); // @ts-ignore scope = _scope; if (skill.contentFuns.length > 0) createContent(); @@ -3654,9 +3663,9 @@ export class Player extends HTMLDivElement { return Array.from(this.iterableGetCards(arg1, arg2)); } /** - * @param { Player } player - * @param { string } [arg1] - * @param { string } [arg2] + * @param { Player } player + * @param { string } [arg1] + * @param { string } [arg2] * @returns { Generator } */ *iterableGetDiscardableCards(player, arg1, arg2) { @@ -5861,7 +5870,7 @@ export class Player extends HTMLDivElement { return get.is.sameNature(natures, naturesx); }; if (next.hasNature("poison")) delete next._triggered; - else if(next.unreal) next._triggered = 2; + else if (next.unreal) next._triggered = 2; next.setContent("damage"); next.filterStop = function () { if (this.source && this.source.isDead()) delete this.source; diff --git a/noname/ui/create/menu/pages/exetensionMenu.js b/noname/ui/create/menu/pages/exetensionMenu.js index d0b8fce07..d23b7b0a5 100644 --- a/noname/ui/create/menu/pages/exetensionMenu.js +++ b/noname/ui/create/menu/pages/exetensionMenu.js @@ -18,6 +18,7 @@ import { } from "../index.js"; import { ui, game, get, ai, lib, _status } from "../../../../../noname.js"; import { nonameInitialized } from "../../../../util/index.js"; +import security from "../../../../util/security.js"; export const extensionMenu = function (connectMenu) { if (connectMenu) return; @@ -334,18 +335,18 @@ export const extensionMenu = function (connectMenu) { var ext = {}; var config = null, help = null; + debugger; // NEED TO VIEW DATA for (var i in dash4.content) { try { if (i == "content" || i == "precontent") { - eval("ext[i]=" + dash4.content[i]); + ({ config, help, return: ext[i] } = security.exec2(`return (${dash4.content[i]});`)); if (typeof ext[i] != "function") { throw "err"; } else { ext[i] = ext[i].toString(); } } else { - eval(dash4.content[i]); - eval("ext[i]=" + i); + ({ config, help, return: ext[i] } = security.exec2(`${dash4.content[i]}; return (${i});`)); if (ext[i] == null || typeof ext[i] != "object") { throw "err"; } else { diff --git a/noname/ui/create/menu/pages/otherMenu.js b/noname/ui/create/menu/pages/otherMenu.js index 83ff92ce0..8c0758c40 100644 --- a/noname/ui/create/menu/pages/otherMenu.js +++ b/noname/ui/create/menu/pages/otherMenu.js @@ -20,6 +20,8 @@ import { getLatestVersionFromGitHub, getTreesFromGithub, } from "../../../../library/update.js"; +import security from "../../../../util/security.js"; +import { AccessAction, Marshal, Monitor } from "../../../../util/sandbox.js"; export const otherMenu = function (/** @type { boolean | undefined } */ connectMenu) { if (connectMenu) return; @@ -1212,73 +1214,84 @@ export const otherMenu = function (/** @type { boolean | undefined } */ connectM ai: ai, cheat: lib.cheat, }); - Object.defineProperties(proxyWindow, { - _status: { - configurable: false, - enumerable: true, - writable: false, - }, - lib: { - configurable: false, - enumerable: true, - writable: false, - }, - game: { - configurable: false, - enumerable: true, - writable: false, - }, - ui: { - configurable: false, - enumerable: true, - writable: false, - }, - get: { - configurable: false, - enumerable: true, - writable: false, - }, - ai: { - configurable: false, - enumerable: true, - writable: false, - }, - cheat: { - configurable: false, - enumerable: true, - writable: false, - }, - }); - proxyWindow = new Proxy(proxyWindow, { - set(target, prop, newValue) { - if (!["_status", "lib", "game", "ui", "get", "ai", "cheat"].includes(prop)) { - Reflect.set(window, prop, newValue); - } - return Reflect.set(target, prop, newValue); - }, - }); + if (security.isSandboxRequired()) { + new Monitor() + .action(AccessAction.DEFINE) + .action(AccessAction.WRITE) + .action(AccessAction.DELETE) + .require("target", proxyWindow) + .require("property", "_status", "lib", "game", "ui", "get", "ai", "cheat") + .then((access, nameds, control) => { + if (access.action == AccessAction.DEFINE) { + control.preventDefault(); + control.stopPropagation(); + control.setReturnValue(false); + return; + } + + control.overrideParameter("target", window); + }) + .start(); + } else { + const keys = ["_status", "lib", "game", "ui", "get", "ai", "cheat"]; + + for (const key of keys) { + const descriptor = Reflect.getOwnPropertyDescriptor(proxyWindow, key); + if (!descriptor) continue; + descriptor.writable = false; + descriptor.enumerable = true; + descriptor.configurable = false; + Reflect.defineProperty(proxyWindow, key, descriptor); + } + + proxyWindow = new Proxy(proxyWindow, { + set(target, propertyKey, value, receiver) { + if (typeof propertyKey == "string" && keys.includes(propertyKey)) { + return Reflect.set(target, propertyKey, value, receiver); + } + + return Reflect.set(window, propertyKey, value); + }, + }); + } //使用new Function隔绝作用域,避免在控制台可以直接访问到runCommand等变量 /** * @type { (value:string)=>any } */ - const fun = new Function( - "window", - ` - const _status=window._status; - const lib=window.lib; - const game=window.game; - const ui=window.ui; - const get=window.get; - const ai=window.ai; - const cheat=window.lib.cheat; - //使用正则匹配绝大多数的普通obj对象,避免解析成代码块。 - const reg=${/^\{([^{}]+:\s*([^\s,]*|'[^']*'|"[^"]*"|\{[^}]*\}|\[[^\]]*\]|null|undefined|([a-zA-Z$_][a-zA-Z0-9$_]*\s*:\s*)?[a-zA-Z$_][a-zA-Z0-9$_]*\(\)))(?:,\s*([^{}]+:\s*(?:[^\s,]*|'[^']*'|"[^"]*"|\{[^}]*\}|\[[^\]]*\]|null|undefined|([a-zA-Z$_][a-zA-Z0-9$_]*\s*:\s*)?[a-zA-Z$_][a-zA-Z0-9$_]*\(\))))*\}$/}; - return function(value){ - "use strict"; - return eval(reg.test(value)?('('+value+')'):value); - } - ` - )(proxyWindow); + let fun + if (security.isSandboxRequired()) { + fun = security.eval(` + const _status=window._status; + const lib=window.lib; + const game=window.game; + const ui=window.ui; + const get=window.get; + const ai=window.ai; + const cheat=window.lib.cheat; + //使用正则匹配绝大多数的普通obj对象,避免解析成代码块。 + const reg=${/^\{([^{}]+:\s*([^\s,]*|'[^']*'|"[^"]*"|\{[^}]*\}|\[[^\]]*\]|null|undefined|([a-zA-Z$_][a-zA-Z0-9$_]*\s*:\s*)?[a-zA-Z$_][a-zA-Z0-9$_]*\(\)))(?:,\s*([^{}]+:\s*(?:[^\s,]*|'[^']*'|"[^"]*"|\{[^}]*\}|\[[^\]]*\]|null|undefined|([a-zA-Z$_][a-zA-Z0-9$_]*\s*:\s*)?[a-zA-Z$_][a-zA-Z0-9$_]*\(\))))*\}$/}; + return function(value){ + "use strict"; + return eval(reg.test(value)?('('+value+')'):value); + }; + `); + } else { + fun = (new Function('window', ` + const _status=window._status; + const lib=window.lib; + const game=window.game; + const ui=window.ui; + const get=window.get; + const ai=window.ai; + const cheat=window.lib.cheat; + //使用正则匹配绝大多数的普通obj对象,避免解析成代码块。 + const reg=${/^\{([^{}]+:\s*([^\s,]*|'[^']*'|"[^"]*"|\{[^}]*\}|\[[^\]]*\]|null|undefined|([a-zA-Z$_][a-zA-Z0-9$_]*\s*:\s*)?[a-zA-Z$_][a-zA-Z0-9$_]*\(\)))(?:,\s*([^{}]+:\s*(?:[^\s,]*|'[^']*'|"[^"]*"|\{[^}]*\}|\[[^\]]*\]|null|undefined|([a-zA-Z$_][a-zA-Z0-9$_]*\s*:\s*)?[a-zA-Z$_][a-zA-Z0-9$_]*\(\))))*\}$/}; + return function(value){ + "use strict"; + return eval(reg.test(value)?('('+value+')'):value); + } + `))(proxyWindow); + } const runCommand = () => { if (text2.value && !["up", "down"].includes(text2.value)) { logindex = -1; diff --git a/noname/util/index.js b/noname/util/index.js index 8cc922af0..ea7c21647 100644 --- a/noname/util/index.js +++ b/noname/util/index.js @@ -7,8 +7,15 @@ export const assetURL = nonameInitialized == "nodejs" ? "" : nonameInitialized; -export const GeneratorFunction = function* () {}.constructor; -export const AsyncFunction = async function () {}.constructor; +/** @type {typeof Function} */ +// @ts-ignore +export const GeneratorFunction = (function* () {}).constructor; +/** @type {typeof Function} */ +// @ts-ignore +export const AsyncFunction = (async function () {}).constructor; +/** @type {typeof Function} */ +// @ts-ignore +export const AsyncGeneratorFunction = (async function* () {}).constructor; export const userAgent = navigator.userAgent.toLowerCase(); export { Mutex } from "./mutex.js"; export const characterDefaultPicturePath = "image/character/default_silhouette_";