Переглянути джерело

ref: remove prototype chain from some constant objects

Sv443 1 місяць тому
батько
коміт
be2467a82b
4 змінених файлів з 28 додано та 16 видалено
  1. 10 8
      src/config.ts
  2. 5 4
      src/constants.ts
  3. 4 4
      src/interface.ts
  4. 9 0
      src/utils/misc.ts

+ 10 - 8
src/config.ts

@@ -1,6 +1,6 @@
 import { DataStore, compress, type DataMigrationsDict, decompress, type LooseUnion, clamp } from "@sv443-network/userutils";
 import { enableDiscardBeforeUnload, featInfo } from "./features/index.js";
-import { compressionSupported, error, getVideoTime, info, log, reloadTab, t, type TrLocale } from "./utils/index.js";
+import { compressionSupported, error, getVideoTime, info, log, pureObj, reloadTab, t, type TrLocale } from "./utils/index.js";
 import { emitSiteEvent } from "./siteEvents.js";
 import { compressionFormat } from "./constants.js";
 import { emitInterface } from "./interface.js";
@@ -11,14 +11,16 @@ import { showPrompt } from "./dialogs/prompt.js";
 /** If this number is incremented, the features object data will be migrated to the new format */
 export const formatVersion = 9;
 
-export const defaultData = (Object.keys(featInfo) as (keyof typeof featInfo)[])
-  // @ts-ignore
-  .filter((ftKey) => featInfo?.[ftKey]?.default !== undefined)
-  .reduce<Partial<FeatureConfig>>((acc, key) => {
+export const defaultData = pureObj(
+  (Object.keys(featInfo) as (keyof typeof featInfo)[])
     // @ts-ignore
-    acc[key] = featInfo?.[key]?.default as unknown as undefined;
-    return acc;
-  }, {}) as FeatureConfig;
+    .filter((ftKey) => featInfo?.[ftKey]?.default !== undefined)
+    .reduce<Partial<FeatureConfig>>((acc, key) => {
+      // @ts-ignore
+      acc[key] = featInfo?.[key]?.default as unknown as undefined;
+      return acc;
+    }, {}) as FeatureConfig
+);
 
 /** Config data format migration dictionary */
 export const migrations: DataMigrationsDict = {

+ 5 - 4
src/constants.ts

@@ -1,5 +1,6 @@
 import { randomId } from "@sv443-network/userutils";
 import { LogLevel } from "./types.js";
+import { pureObj } from "./utils/misc.js";
 
 type ConstTypes = {
   mode: "production" | "development";
@@ -47,11 +48,11 @@ export const changelogUrl = `https://raw.githubusercontent.com/${repo}/${buildNu
 export const initialParams = new URL(location.href).searchParams;
 
 /** Names of platforms by key of {@linkcode host} */
-export const platformNames = {
+export const platformNames = pureObj({
   github: "GitHub",
   greasyfork: "GreasyFork",
   openuserjs: "OpenUserJS",
-} as const;
+} as const);
 
 /** Default compression format used throughout BYTM */
 export const compressionFormat: CompressionFormat = "deflate-raw";
@@ -78,8 +79,8 @@ export const sessionStorageAvailable =
 export const defaultLogLevel: LogLevel = mode === "production" ? LogLevel.Info : LogLevel.Debug;
 
 /** Info about the userscript, parsed from the userscript header (tools/post-build.js) */
-export const scriptInfo = {
+export const scriptInfo = pureObj({
   name: GM.info.script.name,
   version: GM.info.script.version,
   namespace: GM.info.script.namespace,
-} as const;
+} as const);

+ 4 - 4
src/interface.ts

@@ -1,7 +1,7 @@
 import * as UserUtils from "@sv443-network/userutils";
 import * as compareVersions from "compare-versions";
 import { mode, branch, host, buildNumber, compressionFormat, scriptInfo, initialParams, sessionStorageAvailable } from "./constants.js";
-import { getDomain, waitVideoElementReady, getResourceUrl, getSessionId, getVideoTime, log, setLocale, getLocale, hasKey, hasKeyFor, t, tp, type TrLocale, info, error, onInteraction, getThumbnailUrl, getBestThumbnailUrl, fetchVideoVotes, setInnerHtml, getCurrentMediaType, tl, tlp, PluginError, formatNumber, reloadTab, getVideoElement, getVideoSelector } from "./utils/index.js";
+import { getDomain, waitVideoElementReady, getResourceUrl, getSessionId, getVideoTime, log, setLocale, getLocale, hasKey, hasKeyFor, t, tp, type TrLocale, info, error, onInteraction, getThumbnailUrl, getBestThumbnailUrl, fetchVideoVotes, setInnerHtml, getCurrentMediaType, tl, tlp, PluginError, formatNumber, reloadTab, getVideoElement, getVideoSelector, pureObj } from "./utils/index.js";
 import { addSelectorListener } from "./observers.js";
 import { defaultData, getFeatures, setFeatures } from "./config.js";
 import { autoLikeStore, featInfo, fetchLyricsUrlTop, getLyricsCacheEntry, sanitizeArtists, sanitizeSong } from "./features/index.js";
@@ -108,7 +108,7 @@ export const allInterfaceEvents = [
  * All functions that can be called on the BYTM interface using `unsafeWindow.BYTM.functionName();` (or `const { functionName } = unsafeWindow.BYTM;`)  
  * If prefixed with /\*🔒\*\/, the function is authenticated and requires a token to be passed as the first argument.
  */
-const globalFuncs: InterfaceFunctions = {
+const globalFuncs: InterfaceFunctions = pureObj({
   // meta:
   /*🔒*/ getPluginInfo,
 
@@ -167,7 +167,7 @@ const globalFuncs: InterfaceFunctions = {
 
   // other:
   formatNumber,
-};
+});
 
 /** Initializes the BYTM interface */
 export function initInterface() {
@@ -211,7 +211,7 @@ export function setGlobalProp<
   const win = getUnsafeWindow();
 
   if(typeof win.BYTM !== "object")
-    win.BYTM = {} as BytmObject;
+    win.BYTM = pureObj({}) as BytmObject;
 
   win.BYTM[key] = value;
 }

+ 9 - 0
src/utils/misc.ts

@@ -251,6 +251,15 @@ export function isStringGen(val: unknown): val is StringGen {
     || val instanceof Promise;
 }
 
+/**
+ * Turns the passed object into a pure object without a prototype chain and without default properties like `toString`, `__defineGetter__`, etc.  
+ * This makes the object immune to prototype pollution attacks and allows for cleaner object literals, at the cost of being harder to work with in some cases.  
+ * It also effectively transforms a `Stringifiable` value into one that will throw a TypeError when stringified instead of defaulting to `[object Object]`
+ */
+export function pureObj<TObj extends object>(obj: TObj): TObj {
+  return Object.assign(Object.create(null), obj);
+}
+
 //#region resources
 
 /**