Back to Blog
high SEVERITY5 min read

TweenMax `_applyCycle` Prototype Pollution via vars.cycle Keys

A bundled copy of the TweenMax animation library copied attacker-influenceable `vars.cycle` property names straight onto a tween configuration object using an unguarded `for...in` loop, so a key named `__proto__`, `constructor`, or `prototype` was written through to the object's prototype chain. The fix adds an explicit key denylist to both copies of the `_applyCycle` helper so those three names are skipped during the merge. No CVE or GHSA is assigned; the issue is tracked as CWE-1321 (Improperl

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 7, 2026•Reviewed October 7, 2026

Answer Summary

The affected code is a vendored build of the TweenMax/GSAP animation library, specifically the internal `_applyCycle` helper invoked by `TweenMax.staggerTo()` and `TweenMax.staggerFrom()` when a `vars.cycle` object is supplied; no published package version range was determined. An attacker who controls any part of that animation configuration could include a property named `__proto__`, `constructor`, or `prototype` and have it assigned through the `vars[p] = val` write, replacing the configuration object's prototype or shadowing its `constructor` — which lets inherited, never-declared properties such as callback hooks and overwrite flags be smuggled into the tween, and breaks `constructor`-based plain-object checks in downstream code. The fix adds a three-key denylist that `continue`s past `__proto__`, `constructor`, and `prototype` in both copies of the cycle-merge loop; because this is first-party vendored code, there is no fixed package version, only the hardening commit. The weakness class is CWE-1321, Improperly Controlled Modification of Object Prototype Attributes.

Vulnerability at a Glance

cweCWE-1321
fixSkip the three dangerous key names with an explicit `continue` guard in both cycle-merge loops
riskAttacker-supplied animation config can hijack the prototype of tween `vars` objects, inject inherited callbacks/flags, and shadow `constructor` to defeat plain-object type checks
languageJavaScript
root cause`_applyCycle` copies every key from `vars.cycle` with `for (p in alt) { vars[p] = ... }` and never rejects `__proto__`, `constructor`, or `prototype`
vulnerabilityPrototype pollution through unfiltered object merge

Summary

A bundled copy of the TweenMax animation library copied attacker-influenceable vars.cycle property names straight onto a tween configuration object using an unguarded for...in loop, so a key named __proto__, constructor, or prototype was written through to the object's prototype chain. The fix adds an explicit key denylist to both copies of the _applyCycle helper so those three names are skipped during the merge. No CVE or GHSA is assigned; the issue is tracked as CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes).

Introduction

TweenMax's stagger APIs accept a single configuration object and fan it out across N targets. The mechanism that makes each target's animation slightly different is the cycle property: you hand staggerTo() a vars object whose cycle sub-object maps property names to either an array of per-index values or a function that returns one. Internally, a small helper — _applyCycle(vars, targets, timeline) — walks that cycle object and writes the resolved value back onto the tween's own vars.

That walk was a bare for...in loop with a direct dynamic assignment:

p,
val;
for (p in alt) {
  val = alt[p];
  vars[p] =
    typeof val === 'function'
      ? /* resolve per-target value */
      : /* index into the array */;
}

alt is the user-supplied cycle object. p is a key name that came from outside the library. vars[p] = … is a computed-property write with no allowlist, no denylist, and no hasOwnProperty filter. In JavaScript, vars['__proto__'] = someObject does not create an own property — it invokes the inherited __proto__ setter and replaces the prototype of vars. vars['constructor'] = x shadows constructor for every later type check that reads it. That is CWE-1321 in three lines of library code, and because _applyCycle runs once per target in a stagger, a single call applies the attacker's key to every configuration object the stagger creates.

If you have ever written a "just copy the options the caller gave me" loop, this is the exact shape to recognize.

Affected Versions

Affected not applicable (first-party code) — a vendored TweenMax build in this repository
Fixed in not applicable (first-party code) — resolved by the hardening commit described below
Ecosystem unknown (vendored JavaScript bundle, not resolved through a package manager)
CVE / GHSA not assigned
CWE CWE-1321 — Improperly Controlled Modification of Object Prototype Attributes

Because the library is checked in as a bundled asset rather than pulled from a registry, there is no version range to upgrade past. The exposure is determined by whether your application passes externally-influenced data into a vars.cycle object.

The Vulnerability Explained

The vulnerable pattern

Here is the pre-fix loop, verbatim from the patched helper:

p,
val;
for (p in alt) {
  val = alt[p];
  vars[p] =
    typeof val === 'function'

Three distinct problems stack up in those lines:

  1. p is fully attacker-chosen. It is whatever key appears on cycle. The library's contract is "any tween property name," which is implicitly a wildcard.
  2. vars[p] = … is a sink, not a copy. For p === '__proto__', the assignment goes through the accessor defined on Object.prototype, which mutates the [[Prototype]] of vars. For p === 'constructor' or p === 'prototype', it shadows a name that other code reads for type dispatch.
  3. for...in enumerates inherited properties too. If anything else in the runtime has already polluted Object.prototype, this loop will faithfully pick those keys up and copy them into vars — so the helper is both a pollution sink and a pollution amplifier.

The same helper is inlined twice in the shipped bundle: once in the embedded TweenLite core and once in the TweenMax layer that defines staggerTo. Both copies had the identical unguarded loop.

Attack scenario

Consider a dashboard, page builder, or themeable widget where motion settings are data. A saved layout, a CMS field, or a plugin manifest supplies the animation descriptor, and the app does something like:

// `motion` is deserialized from stored/untrusted configuration
TweenMax.staggerTo(cards, 0.5, motion, 0.1);

An attacker-authored descriptor:

{
  "y": 0,
  "cycle": {
    "__proto__": [{ "overwrite": "all", "immediateRender": true }],
    "constructor": ["not-a-function"]
  }
}

When _applyCycle runs:

  • vars['__proto__'] = { overwrite: 'all', immediateRender: true } swaps the prototype of the tween config. From that moment, vars.overwrite and vars.immediateRender read as truthy even though neither was ever declared by the application. GSAP reads a long list of properties off vars — lifecycle hooks (onStart, onUpdate, onComplete), callbackScope, paused, delay, overwrite, immediateRender — and simply checks whether they are present. Inherited properties satisfy that check exactly like own properties. Where the configuration source can carry functions rather than pure JSON (a JS-authored theme file, a plugin module, a deserializer that rehydrates callables), this is a direct route to getting a callback invoked that the application never registered.
  • vars['constructor'] = 'not-a-function' is the quieter half. Enormous amounts of JavaScript decide "is this a plain object?" with obj.constructor === Object or obj.constructor.name. Flipping that answer on a config object that then gets passed to a deep-merge, a cloner, or a serializer can turn a safe shallow copy into a recursive one — and a recursive merge over an object that still has __proto__ keys in its payload is how local prototype hijacking becomes process-wide Object.prototype pollution.

Real-world impact

For a service rendering untrusted animation configuration, the realistic outcomes are: animation semantics silently overridden (including overwrite: "all", which kills other tweens on the same targets), inherited properties appearing on objects that should have had none, constructor-based type guards returning the wrong answer, and — if a downstream nested write occurs through the hijacked chain — polluted Object.prototype that affects every object literal in the page, which is the classic setup for authorization-flag spoofing and DOM-based script injection in the surrounding application.

Note that this fix is hardening applied by review; no automated test suite in the repository exercised the stagger path, so it was not validated by a reproduction harness.

The Fix

The change adds a three-name denylist at the top of the loop body, in both inlined copies of the helper.

Before:

```js
for (p in alt) {
val = alt[

Prevention and further reading

Frequently Asked Questions

Which TweenMax API has to be reachable by untrusted input for this to matter?

`TweenMax.staggerTo()` / `staggerFrom()` (and the `TimelineMax` equivalents) called with a `vars` object that contains a `cycle` property. If `vars.cycle` is entirely hard-coded in your own source, the loop never sees an attacker-chosen key name.

Why were two nearly identical loops patched instead of one?

The bundled build inlines the cycle-merge helper twice — once in the embedded TweenLite core and once in the TweenMax layer that adds `staggerTo` — so the same `for (p in alt)` body exists in two places and both had to receive the denylist, or the unpatched copy would still accept `__proto__`.

Does the denylist cover the `staggerTo` copy loop that duplicates `vars` into `copy`?

No. The shipped change only guards the two cycle-merge loops; the separate `for...in` that clones `vars` into the per-target `copy` object still has no key filter, so that loop remains follow-up work and is worth hardening with the same guard plus a `hasOwnProperty` check.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #124

Related Articles

high

picomatch 2.3.1 ReDoS: Extglob Pattern Catastrophic Backtracking

picomatch versions below 2.3.2, 3.0.2, or 4.0.4 contain a Regular Expression Denial of Service vulnerability in extglob pattern parsing. An attacker can cause catastrophic backtracking with patterns containing nested alternations and quantifiers, freezing any Node.js process that evaluates untrusted glob expressions.

critical

pet-window.js Dynamic Code Evaluation: CWE-94 Hardening via Number

The pet-window module constructed dynamic JavaScript by embedding raw configuration values into code strings. An attacker with local access could inject arbitrary JavaScript by modifying stored configuration. The fix replaces string interpolation with explicit Number() coercion and NaN validation for all numeric configuration parameters.

high

sanitizeUnicodeInput(): Fullwidth U+ Bypasses Codepoint Validation

The `sanitizeUnicodeInput()` helper used by the project character-range settings screen rewrote `U+` prefixes to `0x` and called `parseInt()`, but never normalized its argument first. Compatibility-equivalent forms such as fullwidth `U+`, superscript digits, or mathematical alphanumerics never matched the `/U\+/gi` regex, fell through to the `else return inputString` branch, and were handed back to callers verbatim as "sanitized" values. The fix inserts a `String.prototype.normalize('NFKC')` pas

high

brace-expansion Stack Exhaustion: CVE-2026-102276 Patched

A critical stack exhaustion vulnerability in brace-expansion allows attackers to crash Node.js applications by supplying specially crafted brace patterns that trigger unbounded recursion. The fix upgrades the library across all maintained version lines to enforce depth limits on recursive expansion. This vulnerability affects any service that expands user-controlled brace patterns without input validation.

critical

BFF Proxy QR Code Endpoint Prototype Pollution via Unvalidated JSON

A critical prototype pollution vulnerability in a backend-for-frontend (BFF) proxy endpoint allowed attackers to inject malicious properties into the JavaScript Object prototype by crafting JSON requests with forbidden keys. This could compromise application behavior across all objects. The fix adds explicit key validation to reject payloads containing `__proto__`, `constructor`, or `prototype`.

high

xcb_get_image_reply() NULL Deref on malloc() Failure

The XCB image-reply handler in the screen-streaming bridge allocated a buffer with `malloc()` and immediately wrote to it with `memset()` and `memcpy()` without checking for allocation failure. Under memory pressure, this produces a null-pointer dereference that crashes the process, turning a resource-exhaustion condition into an immediate denial of service. The fix adds a NULL check that frees the intermediate reply and returns early instead of writing to invalid memory.