Skip to content

Conversation

dejour
Copy link

@dejour dejour commented Sep 29, 2025

Fix: Handle iframe sandbox attribute correctly when bound to nullish values

Problem Description

The sandbox attribute on <iframe> elements was not being removed when bound to nullish values (null or undefined) in Vue 3, causing incorrect sandboxing behavior. Instead of removing the attribute entirely, Vue was setting it to the string "null", which applies restrictive sandbox policies.

Issue Details

  • GitHub Issue: 'sandbox' attribute is not removed when bound to a nullish value #13946
  • Affected Element: <iframe> with sandbox attribute
  • Problem: When using :sandbox="null" or :sandbox="undefined", the attribute was set to "null" string instead of being removed
  • Expected Behavior: The sandbox attribute should be completely removed when bound to nullish values
  • Actual Behavior: The sandbox attribute was set to "null" string, enabling restrictive sandboxing

Root Cause Analysis

The issue stems from Vue's property vs attribute handling logic in patchProp. Vue determines whether to treat a binding as a DOM property or HTML attribute using the shouldSetAsProp function:

return key in el  // If property exists on element, treat as property

The <iframe> element has a sandbox DOM property of type string. When Vue sets iframe.sandbox = null, JavaScript's type coercion converts null to the string "null", which then becomes the attribute value.

Why This Is Problematic

  1. Security Implications: Setting sandbox="null" applies sandbox restrictions instead of no sandboxing
  2. Inconsistent with HTML Spec: The HTML specification expects the absence of the sandbox attribute to mean no sandboxing
  3. Vue 2 Compatibility: This behavior differs from Vue 2, which correctly removed the attribute
  4. Developer Expectations: Developers expect nullish values to remove attributes, not set them to string representations

Solution

Added sandbox to the list of attributes that should always be handled as attributes rather than properties in the shouldSetAsProp function.

Code Changes

packages/runtime-dom/src/patchProp.ts:

// #13946 iframe.sandbox should always be set as attribute since setting
// the property to null results in 'null' string, and setting to empty string
// enables the most restrictive sandbox mode instead of no sandboxing.
if (key === 'sandbox') {
  return false
}

This ensures that:

  • sandbox is always handled through the attribute patching path (patchAttr)
  • Nullish values (null/undefined) properly remove the attribute via el.removeAttribute()
  • Empty strings still set an empty attribute (most restrictive sandbox mode)
  • Valid sandbox values are set correctly as attributes

Why This Approach

This follows the same pattern used for other problematic attributes like spellcheck, draggable, translate, and autocorrect that have property/attribute handling conflicts.

Testing

Added comprehensive tests covering all scenarios:

  1. Setting valid sandbox values: Ensures string values are set correctly as attributes
  2. Nullish value removal: Verifies null and undefined completely remove the attribute
  3. Empty string handling: Confirms empty string sets empty attribute (most restrictive mode)
  4. Falsy value handling: Tests that false and 0 remove the attribute
  5. Property vs Attribute verification: Ensures sandbox is always handled as attribute

Impact Assessment

Positive Impact

  • ✅ Fixes security issue where nullish values applied unintended sandbox restrictions
  • ✅ Aligns behavior with HTML specification and developer expectations
  • ✅ Maintains compatibility with Vue 2 behavior
  • ✅ No breaking changes for valid use cases

Related Issues

Migration Guide

No migration needed. This fix corrects incorrect behavior:

Before (incorrect):

<iframe :sandbox="null"> <!-- Results in sandbox="null" -->

After (correct):

<iframe :sandbox="null"> <!-- Attribute is completely removed -->

Developers who were working around this issue by using explicit conditional rendering can now use nullish values directly:

<!-- Old workaround -->
<iframe v-if="sandboxValue" :sandbox="sandboxValue">
<iframe v-else>

<!-- Now works correctly -->
<iframe :sandbox="sandboxValue || null">

Summary by CodeRabbit

  • Bug Fixes
    • iframe sandbox is now always applied and removed as an HTML attribute for consistent behavior; setting it to null/undefined removes the attribute, and an empty string preserves an empty attribute value.
  • Tests
    • Added tests ensuring sandbox is set, removed, and handled as an empty value without invoking element property behavior.

Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

Treats iframe sandbox as an attribute (not a property) in patchProp and adds a unit test verifying set, remove (null/undefined), and empty-string behaviors; no public API changes.

Changes

Cohort / File(s) Summary
Runtime DOM prop handling
packages/runtime-dom/src/patchProp.ts
Adds a special-case in shouldSetAsProp to return false for key 'sandbox' on non-SVG elements, forcing attribute-based handling for iframe sandbox values.
Unit tests for sandbox behavior
packages/runtime-dom/__tests__/patchAttrs.spec.ts
Adds a test that simulates an iframe with a sandbox property getter/setter and asserts that patchProp sets/removes the sandbox attribute (including empty-string behavior) without invoking the property setter.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor V as Virtual DOM
  participant P as patchProp
  participant S as shouldSetAsProp
  participant D as DOM Element

  V->>P: patchProp(el, "sandbox", prev, next, isSVG=false)
  P->>S: shouldSetAsProp(el, "sandbox", isSVG)
  S-->>P: false (force attribute path)
  alt next is null or undefined
    P->>D: removeAttribute("sandbox")
  else next is empty string
    P->>D: setAttribute("sandbox", "")
  else next is non-empty
    P->>D: setAttribute("sandbox", next)
  end
  note over P,D: property setter is not invoked
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I nibble at code with whiskered care,
Sandbox sits as an attribute there.
Null breeze lifts the tag away,
Empty string hops in to stay.
Tests snug in the burrow—hop hooray! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly states the main corrective action, specifying the affected module (runtime-dom), the target element (iframe sandbox), and the purpose (handled as an attribute to prevent unintended behavior), making the primary change immediately understandable.
Linked Issues Check ✅ Passed The changes introduce a short-circuit in shouldSetAsProp to force sandbox to always be patched via attributes and augment tests verifying removal of nullish bindings, correct empty-string handling, and proper attribute assignment without invoking the property setter, directly addressing issue #13946’s requirement to remove the sandbox attribute when bound to nullish values and ensure spec-compliant behavior.
Out of Scope Changes Check ✅ Passed All modifications are confined to patchProp.ts and its tests specifically for sandbox attribute handling, and there are no unrelated edits to other code paths or modules, indicating no out-of-scope changes have been introduced.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc9cf7f and e36aa16.

📒 Files selected for processing (1)
  • packages/runtime-dom/src/patchProp.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (1)
packages/runtime-dom/src/patchProp.ts (1)

114-119: LGTM! The fix correctly forces sandbox to be treated as an attribute.

The implementation properly addresses issue #13946 by preventing property assignment (which coerces null to the string "null"). The tagName check follows the pattern recommended in the past review comment, limiting the workaround to IFRAME elements where it's needed.

One minor consideration: el.tagName returns uppercase in HTML documents, so the comparison with 'IFRAME' is correct. This is consistent with other tagName checks in this file (e.g., lines 128, 133, 139-144).


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/runtime-dom/__tests__/patchAttrs.spec.ts (1)

92-115: Good coverage; add two quick assertions to lock behavior.

  • Add a case for literal string "null" to ensure it’s preserved as an attribute value (not special‑cased).
  • Optionally assert behavior when a non‑string (e.g., false) is passed so expectations are explicit.
   // Verify sandbox is treated as attribute, not property
   patchProp(iframe, 'sandbox', null, 'allow-scripts')
   expect(iframe.getAttribute('sandbox')).toBe('allow-scripts')
 
+  // Literal string "null" should be preserved as-is
+  patchProp(iframe, 'sandbox', 'allow-scripts', 'null')
+  expect(iframe.getAttribute('sandbox')).toBe('null')
+
   // Setting to null should remove the attribute
   patchProp(iframe, 'sandbox', 'allow-scripts', null)
   expect(iframe.hasAttribute('sandbox')).toBe(false)
   expect(iframe.getAttribute('sandbox')).toBe(null)
 
   // Setting to undefined should also remove the attribute
   patchProp(iframe, 'sandbox', null, 'allow-forms')
   expect(iframe.getAttribute('sandbox')).toBe('allow-forms')
   patchProp(iframe, 'sandbox', 'allow-forms', undefined)
   expect(iframe.hasAttribute('sandbox')).toBe(false)
 
   // Empty string should set empty attribute (most restrictive sandbox)
   patchProp(iframe, 'sandbox', null, '')
   expect(iframe.getAttribute('sandbox')).toBe('')
   expect(iframe.hasAttribute('sandbox')).toBe(true)
+
+  // Optional: non-string value clarifies current stringification behavior
+  patchProp(iframe, 'sandbox', '', false as any)
+  expect(iframe.getAttribute('sandbox')).toBe('false')
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a8aa0b and f55d3e1.

📒 Files selected for processing (2)
  • packages/runtime-dom/__tests__/patchAttrs.spec.ts (1 hunks)
  • packages/runtime-dom/src/patchProp.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/runtime-dom/__tests__/patchAttrs.spec.ts (1)
packages/runtime-dom/src/patchProp.ts (1)
  • patchProp (25-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Redirect rules
  • GitHub Check: Header rules
  • GitHub Check: Pages changed
🔇 Additional comments (1)
packages/runtime-dom/src/patchProp.ts (1)

114-119: Scope sandbox exception to <iframe> only
Force attribute binding only for <iframe>; preserve custom element property semantics.

-  if (key === 'sandbox') {
+  // Only meaningful for <iframe>; keep CE prop semantics intact.
+  if (key === 'sandbox' && el.tagName === 'IFRAME') {
     return false
   }

@dejour dejour closed this Sep 29, 2025
@dejour dejour reopened this Sep 29, 2025
// #13946 iframe.sandbox should always be set as attribute since setting
// the property to null results in 'null' string, and setting to empty string
// enables the most restrictive sandbox mode instead of no sandboxing.
if (key === 'sandbox') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be checking el.tagName too.

It isn't checked for spellcheck, draggable, translate and autocorrect because they are global and can apply to any element.

For other special key values we do usually check the tagName.

form is an outlier. It did originally check the tagName, but that check was removed because it applied to so many tags that the bloat wasn't worth it, see 180310c.

I'm not entirely sure why we check the tagName. Presumably it's to reduce the risk of the workarounds leaking out and having unintended consequences.

Copy link
Author

@dejour dejour Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

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.

'sandbox' attribute is not removed when bound to a nullish value
2 participants