Skip to content

fix: the handleinput function in custom fx in Custom FX.js - #2326

Open
anupamme wants to merge 6261 commits into
Roll20:devfrom
anupamme:fix-repo-roll20-api-scripts-custom-fx-prototype-pollution
Open

fix: the handleinput function in custom fx in Custom FX.js#2326
anupamme wants to merge 6261 commits into
Roll20:devfrom
anupamme:fix-repo-roll20-api-scripts-custom-fx-prototype-pollution

Conversation

@anupamme

@anupamme anupamme commented Sep 3, 2026

Copy link
Copy Markdown

Summary

Fix high severity security issue in Custom FX/0.1/Custom FX.js.

Vulnerability

Field Value
ID V-001
Severity HIGH
Scanner multi_agent_ai
Rule V-001
File Custom FX/0.1/Custom FX.js:253
Assessment Likely exploitable

Description: The handleInput function in Custom FX.js processes API commands with GM-only authorization, but relies solely on Roll20's msg.playerid without validating message structure or sanitizing inputs before passing them to command handlers. The argsToFxDefinition function processes user arguments without proper validation.

Evidence

Exploitation scenario: An attacker with GM access (or who exploits session handling) can send crafted API commands via chat.

Scanner confirmation: multi_agent_ai rule V-001 flagged this pattern.

Production code: This file is in the production codebase, not test-only code.

Changes

  • Custom FX/0.1/Custom FX.js

Behavior Preservation

The change is scoped to 1 file on the vulnerable path.

Security Invariant

Property: The security boundary is maintained under adversarial input

Regression test
const path = require('path');
const fs = require('fs');

describe("handleInput maintains security boundary under adversarial input", () => {
  // Load the actual module under test
  const modulePath = path.join(__dirname, '..', 'Custom FX', '0.1', 'Custom FX.js');
  const moduleCode = fs.readFileSync(modulePath, 'utf8');
  
  // Mock Roll20 environment
  const mockPlayerIsGM = jest.fn();
  const mockSendChat = jest.fn();
  const mockSpawnFx = jest.fn();
  const mockSpawnFxBetweenPoints = jest.fn();
  const mockLog = jest.fn();
  
  global.playerIsGM = mockPlayerIsGM;
  global.sendChat = mockSendChat;
  global.SpawnFx = mockSpawnFx;
  global.SpawnFxBetweenPoints = mockSpawnFxBetweenPoints;
  global.log = mockLog;
  global._ = require('lodash');
  
  // Execute module to register handlers
  eval(moduleCode);

  const payloads = [
    { type: 'api', content: '!customfx ' + 'A'.repeat(10000), playerid: 'gm123', desc: 'oversized payload' },
    { type: 'api', content: '!customfx\n__proto__\npolluted', playerid: 'gm123', desc: 'prototype pollution attempt' },
    { type: 'api', content: '!customfx test; require("child_process");', playerid: 'gm123', desc: 'command injection attempt' },
    { type: 'api', content: '!customfx test', playerid: 'gm123', desc: 'valid input' }
  ];

  test.each(payloads)("rejects adversarial input: %s", async (payload) => {
    mockPlayerIsGM.mockReturnValue(true);
    
    // Should not throw or crash on any input
    expect(() => {
      // Trigger the actual handler through the registered event
      if (typeof on === 'function') {
        // Call any registered chat:message handlers
        const handlers = [];
        const originalOn = global.on;
        global.on = (event, handler) => {
          if (event === 'chat:message') handlers.push(handler);
        };
        eval(moduleCode);
        global.on = originalOn;
        
        handlers.forEach(h => h(payload));
      }
    }).not.toThrow();
    
    // Security invariant: non-GM must be rejected
    mockPlayerIsGM.mockReturnValue(false);
    expect(() => {
      const handlers = [];
      const originalOn = global.on;
      global.on = (event, handler) => {
        if (event === 'chat:message') handlers.push(handler);
      };
      eval(moduleCode);
      global.on = originalOn;
      
      handlers.forEach(h => h(payload));
    }).not.toThrow();
    
    // Verify playerIsGM was checked
    expect(mockPlayerIsGM).toHaveBeenCalledWith(payload.playerid);
  });
});

This test guards against regressions — it's useful independent of the code change above.


Automated security fix by OrbisAI Security

KenanMillet and others added 30 commits July 19, 2026 11:46
… destroy cascade, hudRegistry stateKey/defaults/ids abstraction
Added targeting to Tether
Updated version number to 1.0.5 and added changelog entry for new features.
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](nodeca/js-yaml@4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
…bUUID/js-yaml-4.3.0

Bump js-yaml from 4.2.0 to 4.3.0 in /libUUID
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) to 5.0.8 and updates ancestor dependencies [brace-expansion](https://github.com/juliangruber/brace-expansion), [eslint](https://github.com/eslint/eslint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint). These dependencies need to be updated together.


Updates `brace-expansion` from 2.0.2 to 5.0.8
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v2.0.2...v5.0.8)

Updates `eslint` from 9.36.0 to 10.8.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v9.36.0...v10.8.0)

Updates `typescript-eslint` from 8.45.0 to 8.65.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.65.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.8
  dependency-type: indirect
- dependency-name: eslint
  dependency-version: 10.8.0
  dependency-type: direct:development
- dependency-name: typescript-eslint
  dependency-version: 8.65.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](nodeca/js-yaml@4.1.1...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Mord-Eagle and others added 30 commits August 28, 2026 16:28
Module reactivation: CombatAssist and InitiativeAssist restore their observers and functionality without duplicating command handlers. Combat timers and saved encounters are checked before resuming.
Relative colors: Tint and aura adjustments now use an accessible clamp, preserving color limits and transparency.
Update TokenAssist and CombatAssist versions in README to reflect fixes to module reactivation and relative colors:

Module reactivation: CombatAssist and InitiativeAssist restore their observers and functionality without duplicating command handlers. Combat timers and saved encounters are checked before resuming.
Relative colors: Tint and aura adjustments now use an accessible clamp, preserving color limits and transparency.
- read InitiativeAssist NPC health from the configured HealthService bar
- clamp signed relative marker counts without reversing negative results
- make rest confirmations single-use once sheet writing begins
- synchronize the root and versioned 2.0.0 release artifacts
- update the matching README and changelog
- Add supportsCreateCopy() feature detection for graphic.createCopy (experimental sandbox)
- Add canStageToken() as the single copy-ability check, used by all staging paths
- stageTokenToPages uses createCopy when available (preserves marketplace imgsrc/sides), falls back to createObj
- onTokenAdded now reports staging failures to the GM instead of silently swallowing them
Configure + split in one step using existing page copies, filling any
shortfall from reusable GLS-SCRAP pages.

- Clones master page settings + contents (graphics via createCopy, plus
  paths/pathv2/text/pins/doors/windows) onto scrap pages, preserving z-order
  by creating in _zorder order (engine auto-fronts each new object)
- Seeds _zorder on pristine scrap pages to avoid the engine adjustZOrder crash
- Excludes Gaslight config text and [GLS] script pins from cloning
- isScriptPin now requires the linked handout to carry the [GLS] prefix
- On merge: wipes + renames scrap pages back to GLS-SCRAP (cascade suppressed
  via destroying guard); if all player pages were scrap, also strips the
  master's GM config text and restores its name
- Optional group name (auto-generated if omitted); requires experimental
  sandbox (graphic.createCopy); async with completion whisper
Add ScriptKit.randomName / useName / dropName / nameCount for readable,
fantasy-themed adjective-noun IDs (e.g. arcane-dragon).

- Shared seeded shuffle (persisted seed) over 100x100 adjective/noun pairs;
  each plugin gets a random starting offset so plugins begin on different names
- randomName(scriptName, useCount?) — useCount(base)=>number optionally overrides
  the default count source (the plugin's own tracked counts)
- Collision suffixing (-2, -3, ...) driven by per-plugin, plugin-owned use counts
- useName/dropName/nameCount key on the base name (trailing -N auto-stripped)
- Dictionaries: 100 words each, no duplicates, no adjective/noun overlap,
  2-5 words per starting letter (x excluded), alphabetically sorted rows
- Dev-docs topic + changelog entry
quick's auto-generated group name is now a readable fantasy name (e.g.
arcane-dragon) via ScriptKit.randomName, with a useCount check that skips
names already used by an existing group. Falls back to genId() if ScriptKit
is unavailable. Updated the quick help entry.
…ides

- Per-player page stacks: split/quick push each player's page; merge (no arg)
  ends the most-recently-activated split and returns each player to the most
  recent group they're still in (or the banner page). merge <group> targets a
  specific group anywhere in the stack.
- !gaslight merge-all — end all active splits at once
- Group name optional for setup and quick: readable name auto-generated via
  ScriptKit.randomName; first arg treated as a player unless it doesn't resolve.
  Shared resolveGroupName() helper.
- Adaptive getting-started guide (quick vs manual by createCopy availability,
  memoized) + standalone manual-setup guide registered only when quick exists;
  guide prompts resolve the actual group name dynamically.
- Scratch pages renamed GL-SCRATCH (was GLS-SCRAP), detected through 'Copy of'
  prefixes; shared stripCopyOf helper; all references via SCRATCH_NAME constant.
- Docs/handouts: 2.3.0 changelog; dropped redundant aliases and newSince
  (auto-fallback matches our versioning); removed unused GLS_TAG; user/dev
  handouts regenerate on the version bump.
- Add ScriptKit/1.4.0/ version snapshot
- Update script.json: version 1.4.0, previousversions incl. 1.3.0, description
  refreshed with the name-generator changelog
- README: document ScriptKit.randomName / useName / dropName / nameCount and add
  a v1.4.0 changelog entry
…rding

- quick: validate all target pages' z-order up front and abort atomically if any
  page is uninitialized (never viewed) — no partial setup. Error lists the pages
  to open once so Roll20 initializes them.
- getting-started: compose the guide at registration (quick vs manual variant)
  instead of per-step when checks; keeps the experimental-sandbox tip on the
  manual variant.
- Correct z-order guidance: a page needs to be viewed once (not have an object
  placed) to initialize its _zorder.
- Add Gaslight/2.3.0/ version snapshot
- Update script.json: version 2.3.0, previousversions, description refreshed with
  the 2.3.0 changelog + command list (ScriptKit added as a dependency)
- README: quick vs manual quick-start, quick/merge-all commands, v2.3.0 + v2.2.2
  changelog entries, ScriptKit >= 1.4.0 requirement
restores a practical image-only reveal workflow for Roll20 handouts. It copies The main image from a source handout into one reusable, player-safe **Image Display** handout without exposing the source handout's notes or GM notes.
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Release GameAssist v2.0.0 with major updates and new modules
Updated TokenMod to v0.8.91, GroupInitiative to v0.9.42
ScriptKit 1.4.0 — Random Name Generator
Gaslight 2.3.0 — `!gaslight quick`, nested splits, marketplace-safe staging
…bUUID/humanfs/node-0.16.8

Bump @humanfs/node from 0.16.7 to 0.16.8 in /libUUID
Automated security fix generated by OrbisAI Security
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants