element-resize-detector is a small browser-only JavaScript library (~1,550 lines across src/) that emits resize events for arbitrary DOM elements, in browsers or situations where ResizeObserver is unavailable or undesirable. Repository: sso://gnocchi-internal/third_party/element-resize-detector, branch main (mirrored at appsheet-third-party/element-resize-detector).
https://github.com/wnr/element-resize-detector, npm version 1.2.4, upstream revision bae322eff9057a532ec42e324f0ef05d4e509f5f (last_upgrade_date 2021-12-14, per METADATA).src/ (the shipped implementation) and dist/ (the prebuilt browserify bundles element-resize-detector.js and element-resize-detector.min.js). Build tooling (Gruntfile.js, karma.conf.js), test/, and examples/ are development-only.object strategy (default): appends a hidden <object type="text/html" data="about:blank"> — a same-origin nested browsing context — into the target element and subscribes to the inner window's native resize event.scroll strategy: appends a tree of absolutely-positioned, overflow-scrolling <div>s plus a <style> element injected into document.head, and infers size changes from scroll events and CSS animation start events.This fork is a transitive dependency, force-resolved to the Google fork via an overrides entry in jeenee/Nirvana/Content/package.json:
"react-sizeme": { "element-resize-detector": "git+https://appsheet-third-party.googlesource.com/element-resize-detector#4a11fdb85cfe9f03d944f23b9f0231d454e02b53" }
package-lock.json resolves node_modules/element-resize-detector to that exact commit (version 1.2.4). Its only consumer is react-sizeme@3.0.2, whose withSize higher-order component is imported by Nirvana/Content/components/app/primary/AdjustableDash.tsx — the resizable dashboard layout in the AppSheet app runtime. The library is therefore genuinely executed in production end-user sessions, on customer app pages.
Two commits, both Trusted Types remediation (go/trusted-types-appsheet), no functional divergence from upstream 1.2.4:
| Commit | Change |
|---|---|
3a3dda5 “Initial import” | Import of upstream 1.2.4 with the Trusted Types patch already applied to src/: in src/detection-strategy/scroll.js, injectScrollStyle‘s inner injectStyle sets styleElement.textContent = style instead of upstream’s styleElement.innerHTML = style. |
4a11fdb “Updated the dist folder” (HEAD) | Rebuilt dist/element-resize-detector.js and dist/element-resize-detector.min.js. The commit message notes the previously imported fork shipped unpatched built files — i.e. src/ was fixed but the bundle consumers actually load was not. This commit closed that gap. |
One upstream innerHTML sink deliberately remains, in src/browser-detector.js: the legacy IE-version probe assigns a static conditional-comment string (div.innerHTML = "<!--[if gt IE " + (++v) + "]><i></i><![endif]-->") to a detached <div>. The interpolated value is a loop counter, not user data.
<object>), injection of a <style> element into document.head, cross-document event-listener registration, mutation of the host page’s inline styles, and setTimeout polling loops.offsetWidth, offsetHeight, computed style) and internally generated numeric IDs. It never reads element text, form values, or app data. Its reporter may console.warn a reference to a DOM element when styles conflict, which could surface page structure in browser logs.<object> URL ever became caller-influenced, CSS/UI-redress issues from injected hidden elements, and listener/detached-DOM leaks causing browser-side denial of service in long-lived sessions.src/detection-strategy/object.js). The injectObject path builds document.createElement("object"), sets object.type = "text/html" and object.data = "about:blank", then appends it into the caller‘s element. about:blank inherits the embedding page’s origin, so its contentDocument is fully same-origin. Verify object.data can never be influenced by caller options, element attributes, or app configuration — a controllable data value here is a direct same-origin script-injection primitive. Also verify the element is not given allow-scripts-equivalent capabilities beyond the default and that object.tabIndex = -1 / aria-hidden="true" remain set so the hidden node cannot be focused or announced.innerHTML, textContent, style.cssText, className, and setAttribute in src/:scroll.js:injectScrollStyle → styleElement.textContent (the patched sink — confirm it has not regressed to innerHTML and that dist/ matches src/).scroll.js:getScrollbarSizes, scroll.js:storeStartSize, scroll.js:initListeners, and object.js:injectObject → repeated element.style.cssText = buildCssTextString([...]) assignments. Confirm every interpolated value is either a hardcoded literal or a number originating from offsetWidth/offsetHeight, never a string from the caller.browser-detector.js → the remaining static-string innerHTML assignment. Confirm the interpolated value stays a loop counter.scroll.js CSS rule construction interpolates containerClass; confirm detectionContainerClass remains the hardcoded "erd_scroll_detection_container" constant and never becomes an option.element-resize-detector.js:listenTo / element-resize-detector.js:uninstall against object.js:addListener/object.js:uninstall and scroll.js:addListener/scroll.js:uninstall. Check that: every addEventListener/attachEvent has a matching removal; the resize listener registered on object.contentDocument.defaultView is detached before the <object> node is removed; the state.checkForObjectDocumentTimeoutId polling loop in object.js:onObjectLoad→getDocument is always cleared; and listener-handler.js:removeAllListeners plus state-handler.js do not retain references to detached elements. Unbounded growth here is a realistic client-side DoS in long-lived AppSheet sessions that mount and unmount dashboards repeatedly.element.style.setProperty("position", "relative", important) and may zero out top/right/bottom/left on statically positioned targets (object.js:alterPositionStyles, scroll.js:alterPositionStyles). Review whether the important: true option, combined with the injected absolutely-positioned containers, can be used to overlay or displace interactive UI (clickjacking / UI-redress) on a customer app page.updateChildSizes/positionScrollbars from its own scroll and animationstart handlers. Check the batch processor (batch-processor@1.0.0) and the updateDetectorElements path for feedback loops that could spin the main thread when an element is resized by its own resize listener.dist/ vs src/ drift. dist/ is a checked-in browserify bundle and is what downstream tooling may load. Commit 4a11fdb exists precisely because these fell out of sync and shipped an unpatched innerHTML. Flag any state where dist/ does not reflect the current src/.| Entry Point | Type | Trusted? | Validation |
|---|---|---|---|
elementResizeDetectorMaker(options) (src/element-resize-detector.js) | Factory / configuration | Yes (called by react-sizeme, first-party bundle code) | getOption applies defaults; strategy is compared against the literals "scroll"/"object" and silently falls back to object; important, debug, callOnAdd coerced with !! |
erd.listenTo([options,] elements, callback) | DOM element handle + callback | Yes (first-party caller supplies the element) | isElement() checks nodeType === 1; isCollection() normalises array-likes; non-elements are rejected via reporter.error |
erd.uninstall(elements) / erd.removeListener / erd.removeAllListeners | Teardown API | Yes | Element state existence checked before teardown |
options.idHandler / options.reporter / options.batchProcessor | Injected strategy objects (callable hooks) | Yes — but they are arbitrary caller-supplied code executed inside the library | None. A hostile or buggy implementation runs with full page privileges; safety rests on these being first-party. |
Browser layout values (offsetWidth, offsetHeight, getComputedStyle) | Ambient DOM / layout engine | Yes | Used as numbers in CSS pixel strings; no string interpolation from user content |
Native resize / scroll / animationstart events on injected nodes | Browser event stream | Yes | Handlers only read geometry from the injected nodes |
Net assessment: there is no path by which customer-authored app data reaches this library as a string. The genuine trust dependency is that the host application passes only its own elements and its own option objects.
about:blank is and remains the only URL loaded into the injected <object>. (c) Injected hidden nodes are inert and will not be traversed or styled by application CSS. (d) document.head is available and writable for <style> injection.<object> nested browsing context. Because the document is about:blank, the child inherits the parent origin and the library reads object.contentDocument and attaches a listener to contentDocument.defaultView directly. This is same-origin by design; it would become a security boundary violation only if the data URL were ever made caller-controllable or cross-origin.| Data Type | Source | Destination | Protection |
|---|---|---|---|
Element geometry (offsetWidth, offsetHeight, computed position/top/right/bottom/left) | Browser layout engine, via getComputedStyle in scroll.js:storeStartSize and object.js | In-memory element state (state-handler.js) and interpolated into generated CSS pixel strings | Numeric values only; never persisted or transmitted |
| Detector element IDs | id-generator.js (monotonic counter) | Element state and injected <style> element id | Sequential integers; not derived from and not correlatable to customer data |
| DOM element references | Caller (react-sizeme → AdjustableDash.tsx) | reporter.warn/reporter.log → browser console when style conflicts or debug mode are active | Console-only, client-side; could surface page structure in user-collected logs. No app data, credentials, or PII. |
No secrets, tokens, credentials, PII, or customer app content pass through this library.
| Action | Location | Guard |
|---|---|---|
Create and append a nested browsing context (<object type="text/html" data="about:blank">) into a caller element | src/detection-strategy/object.js:injectObject (inner mutateDom) | URL is the hardcoded literal "about:blank"; node is marked tabIndex = -1 and aria-hidden="true"; IE ordering handled via browserDetector.isIE() |
| Register a listener on another document's window | src/detection-strategy/object.js:addListener (object.contentDocument.defaultView.addEventListener("resize", listenerProxy)) | Same-origin about:blank child only; removal depends on object.js:uninstall being called |
Inject a <style> element into document.head | src/detection-strategy/scroll.js:injectScrollStyle (inner injectStyle) | Written via textContent (Trusted Types patch, not innerHTML); guarded by a getElementById(styleId) idempotency check; rule text built only from hardcoded constants and the fixed detectionContainerClass |
Bulk style.cssText assignment on injected and host elements | src/detection-strategy/scroll.js:getScrollbarSizes, :storeStartSize, :initListeners; src/detection-strategy/object.js:injectObject | buildCssTextString joins hardcoded rule literals and numeric pixel values; no caller strings interpolated |
| Mutate the host element's inline position styles | src/detection-strategy/object.js:alterPositionStyles, src/detection-strategy/scroll.js:alterPositionStyles (element.style.setProperty) | Applied only when computed position is static; !important applied only when options.important is set; conflicts reported via reporter.warn |
| Schedule recurring timers | src/detection-strategy/object.js:onObjectLoad → getDocument (setTimeout(..., 100) poll for contentDocument) | Timer id stored in element state and cleared on re-entry and in object.js:uninstall |
| Remove injected nodes and all listeners | src/element-resize-detector.js:uninstall → listener-handler.js:removeAllListeners, strategy uninstall | Element state existence checked; correctness of full teardown is a priority review area |
<object> injection in src/detection-strategy/object.js (highest value). This is the only place the library creates a new browsing context. data is the hardcoded "about:blank" today, and because that inherits the embedding origin, anything that made the URL caller-influenceable — an option, an attribute read off the target element, an app-config value — would turn a layout utility into a same-origin script-injection sink. Confirm the literal is unreachable from configuration and that the injected node cannot be re-pointed after insertion.src/dist parity. The entire reason this fork exists is scroll.js:injectScrollStyle using textContent instead of innerHTML, and commit 4a11fdb exists because dist/ once shipped without that fix. Verify the patch is present in src/ and in both dist/ bundles, and treat any reintroduction of innerHTML in src/detection-strategy/ as a release blocker. The residual static-string innerHTML in src/browser-detector.js should be confirmed unreachable with non-literal data (and is a candidate for removal, since the IE probe is dead code in supported browsers).buildCssTextString into style.cssText, and CSS rule text in injectScrollStyle interpolates a class name. These are the library's string-building hot spots. Confirm all inputs remain hardcoded literals or layout-engine numbers, and that detectionContainerClass is not promoted to a caller option — a caller-supplied class name would become a CSS-injection sink inside a live <style> element.object.contentDocument.defaultView, the checkForObjectDocumentTimeoutId polling loop, scroll/animationstart handlers on injected nodes, and the element→state map in state-handler.js all persist until uninstall runs. AppSheet mounts and unmounts dashboards repeatedly within a single session, so incomplete teardown accumulates detached DOM and listeners and degrades or hangs the tab. Verify react-sizeme's unmount path actually reaches erd.uninstall.position: relative (optionally !important) on observed elements and inserts absolutely-positioned hidden containers, including one temporarily inserted at document.body.firstChild in scroll.js:getScrollbarSizes. Review for overlay/clickjacking potential and for interference with AppSheet's own z-index and modal stacking on customer app pages.idHandler, reporter, batchProcessor). These options let arbitrary functions execute inside the library's DOM-mutating code paths with full page privileges. Confirm AppSheet (via react-sizeme) supplies only defaults, and that none of these can be reached from app-author-controlled configuration.Teams can fill out this section with any known filed vulnerability bugs that they've determined should be out of scope from future Fortify bug filing.