Skip to main content

Shared-namespace metafield rendered as active content

Investigates metafield and metaobject values from shared namespaces, which other installed apps can write, that reach HTML, JavaScript, or URL output, and shows how to fix them.

DetailValue
Check IDSHARED_NAMESPACE_METAFIELD_RENDER
SeverityMedium
How it runsAgent review

Merchant-owned metafields and metaobjects can be written by the merchant and by every other app installed on the shop that has write access to that resource. If one of those apps is compromised, then an attacker can overwrite a value that your app renders. A value in a shared namespace is lower-trust input, even when your app is the only app that's meant to write it.

This check reports a shared-namespace value that reaches HTML, JavaScript, or URL output without escaping or serialization for that context. It also reports a value that's deliberately rendered as raw HTML, script, or a URL, such as widget markup that your app stores in reviews.widget_html. It covers Liquid in theme app extensions, Storefront API responses written to the DOM, Admin API responses rendered in the embedded app, and app proxy responses.

This check is reported alongside the cross-site scripting checks, such as THEME_EXTENSION_XSS and UNSAFE_INNERHTML, not instead of them. It's about who else can write the value. Those checks still require you to escape it.


Anchor to How ownership is decidedHow ownership is decided

Your coding agent decides ownership from the namespace or type prefix in your code. It doesn't read the definitions in shopify.app.toml.

  • App-owned: $app and $app:<sub-namespace>, which you read in Liquid with square brackets, such as product.metafields["$app"].<key>. Also a literal app-- namespace, and the metafields of the Liquid app object, app.metafields. For metaobjects, a $app: type.
  • Shared: Every other namespace or type, such as custom, reviews, or specs. This includes a bare app namespace in Liquid, such as product.metafields.app.<key>, which isn't translated to your app's namespace. It also includes shopify-- namespaces and standard definitions: Shopify controls their structure, but merchants and apps can write their values.
  • Unresolved: A namespace that's built at runtime, such as a namespace variable passed to a query. Your agent names the value instead of guessing its ownership.

Any app-- prefix is treated as app-owned, even though it could belong to a different app.

An app-owned value isn't automatically safe. The merchant can still edit it when the definition allows it, and the cross-site scripting checks still require escaping.


extensions/reviews/blocks/reviews-widget.liquid

{% comment %}
Widget markup that this app writes to the shared reviews
namespace, rendered raw. Any other app with write access to
products can replace it with script that runs on every
product page.
{% endcomment %}
<div class="reviews-widget">
{{ product.metafields.reviews.widget_html }}
</div>

Move the field to an app-owned namespace with a declared definition, prefer typed fields such as rich text or URL, or escape the value for its context.

extensions/reviews/blocks/reviews-widget.liquid

{% comment %}
Store the content in an app-owned rich text metafield, so other
apps can't write it, and let metafield_tag render the markup.
Read app-owned metafields through the $app prefix. A bare app
namespace isn't app-owned.
{% endcomment %}
<div class="reviews-widget">
{{ product.metafields["$app"].review_summary | metafield_tag }}
</div>

Declare the app-owned definition in shopify.app.toml. Use access.admin = "merchant_read" if merchants shouldn't edit the value, and access.storefront = "public_read" if the Storefront API needs it.

shopify.app.toml

[product.metafields.app.review_summary]
name = "Review summary"
type = "rich_text_field"
access.admin = "merchant_read"

Don’t

  • Store HTML, script, or tracking code in a shared namespace, such as custom or reviews, and render it raw because your app wrote it.
  • Treat shopify-- namespaces or standard definitions as trusted. Merchants and other apps can write their values.

Do

  • Store values that your app renders in an app-owned namespace, declared as [<resource>.metafields.app.<key>] in shopify.app.toml, and read them in Liquid through the $app prefix, such as product.metafields["$app"].<key>.
  • Prefer typed fields over HTML strings, such as a rich_text_field rendered with metafield_tag, or a url field with a validated scheme.
  • Otherwise, escape or serialize the value with json for the context where it's output.

For more detail, see Metafield ownership.



Was this page helpful?