Widget Security with Iframe Embedding and Domain Restrictions in PropertyWebBuilder

PropertyWebBuilder protects tenant widgets through server-side domain validation in the ApiPublic::V1::WidgetsController, allowing property managers to whitelist specific domains while serving content via isolated iframes.

PropertyWebBuilder is an open-source real estate platform built with Ruby on Rails that enables tenants to embed property listings on external websites. The widget security with iframe embedding and domain restrictions system ensures that only approved domains can access tenant data, preventing unauthorized usage while maintaining seamless integration for legitimate partners through server-side origin checks.

How Domain Restrictions Work

The security model relies on a whitelist approach managed in the database and enforced at the API layer, keeping validation logic away from client-side code where it could be circumvented.

The WidgetConfig Model

The Pwb::WidgetConfig class in app/models/pwb/widget_config.rb stores widget configurations and domain restrictions. It provides the domain_allowed? method (lines 135-151) that validates request origins against the allowed_domains column—a PostgreSQL text[] array supporting exact matches, automatic www. prefix stripping, and wildcard subdomains.

When the allowed_domains array is empty, the widget operates in public mode. When populated with entries like example.com, www.another.com, or *.trusted.com, the system restricts access accordingly.

Origin Validation in API Controllers

The ApiPublic::V1::WidgetsController in app/controllers/api_public/v1/widgets_controller.rb handles data requests from embedded widgets. Before processing each request, the validate_origin method (lines 84-90) extracts the Origin or Referer header, parses the host, and invokes @widget_config.domain_allowed?.

If validation fails, the controller logs a warning identifying the unauthorized domain. This server-side check prevents attackers from spoofing widget keys or tampering with client-side code to bypass restrictions.

Implementation Details

Generating Iframe Embed Code

Tenants generate embed snippets using the iframe_embed_code method (lines 165-180), which produces HTML pointing to /widget/:widget_key on the tenant's host. The generated iframe isolates the widget's DOM from the host page, preventing cross-site scripting while allowing the external site to display listings.


# Example embed code generation

<pre id="iframe-embed-code"><%= @widget.iframe_embed_code(host: request.host_with_port) %></pre>

The resulting HTML creates a sandboxed container:

<iframe
  src="https://mytenant.propertywebbuilder.com/widget/abc123xyz"
  width="100%"
  height="600"
  frameborder="0"
  style="border: none; width: 100%; min-height: 600px;"
  loading="lazy"
  title="Property Listings">
</iframe>

Domain Whitelist Logic

The domain_allowed? implementation handles complex matching scenarios:

def domain_allowed?(domain)
  return true if allowed_domains.blank?          # No restrictions

  return false if domain.blank?

  normalized = domain.to_s.downcase.gsub(/^www\./, '')

  allowed_domains.any? do |allowed|
    pattern = allowed.downcase.gsub(/^www\./, '')
    if pattern.start_with?('*.')
      normalized.end_with?(pattern[1..]) || normalized == pattern[2..]
    else
      normalized == pattern
    end
  end
end

This supports www. automatic removal, exact matches, and wildcard subdomains like *.sub.example.com.

CORS and Frame Options Configuration

The WidgetsController in app/controllers/widgets_controller.rb (lines 39-45) sets permissive headers to allow cross-origin requests while maintaining security through origin validation:

def set_cors_headers
  response.headers['Access-Control-Allow-Origin']  = '*'
  response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
  response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
  response.headers['X-Frame-Options'] = 'ALLOWALL'   # iframe can be embedded anywhere

end

The X-Frame-Options: ALLOWALL header permits embedding on any site, but the data remains protected because the API layer validates every request's origin.

Security Workflow

When an external site loads the widget, the following sequence occurs:

  1. The external page includes the snippet generated by WidgetConfig#iframe_embed_code.
  2. The browser requests https://<widget-host>/widget/<widget_key> via WidgetsController#iframe, which renders app/views/layouts/widget.html.erb.
  3. The widget's JavaScript makes API calls to /api_public/v1/widgets/:widget_key/....
  4. Each request passes through ApiPublic::V1::WidgetsController#validate_origin, which extracts the caller's domain and runs WidgetConfig#domain_allowed?.
  5. If the domain matches the whitelist or the list is empty, the API returns property data; otherwise, the request is logged and can be blocked.

This architecture ensures that even if an attacker obtains the widget key, they cannot access data from unauthorized domains because the security check occurs server-side using headers that browsers enforce.

Configuration Examples

Configure allowed domains via Rails console or admin interface:

widget = Pwb::WidgetConfig.find_by(widget_key: 'abc123xyz')
widget.update!(
  allowed_domains: ['example.com', '*.sub.example.com', 'www.another.com']
)

The above configuration accepts:

  • example.com and www.example.com
  • sub.example.com and any subdomain of it (wildcard)
  • www.another.com

Summary

  • Server-side validation in ApiPublic::V1::WidgetsController#validate_origin prevents unauthorized domain access regardless of client-side tampering.
  • Flexible domain matching in Pwb::WidgetConfig#domain_allowed? supports exact domains, www. stripping, and wildcard subdomains (*.example.com).
  • Iframe isolation keeps the widget DOM separate from host pages while X-Frame-Options: ALLOWALL enables broad embedding.
  • Empty whitelist mode allows public access when allowed_domains is blank, providing flexibility for open distribution.
  • PostgreSQL array storage uses the text[] column type for efficient domain list management.

Frequently Asked Questions

How does PropertyWebBuilder handle subdomain validation?

The domain_allowed? method automatically strips www. prefixes from both the configured domain and the requesting origin. For wildcard subdomains, patterns starting with *. match the domain and any of its subdomains (e.g., *.example.com matches app.example.com and deep.sub.example.com).

What happens if the allowed_domains list is empty?

When the allowed_domains PostgreSQL array is empty or nil, domain_allowed? returns true, making the widget publicly embeddable on any site. This provides flexibility for tenants who want maximum distribution without domain restrictions.

Where exactly is the origin check performed in the request lifecycle?

The check occurs in ApiPublic::V1::WidgetsController#validate_origin (lines 84-90) before any data queries execute. The method extracts the domain from Origin or Referer headers and validates against the widget's whitelist, ensuring rejected requests never reach the database layer.

Can malicious sites bypass the iframe security by faking the widget key?

No. While the iframe HTML endpoint (/widget/:widget_key) serves content with permissive CORS headers, all property data flows through the API controller which validates the Origin header against the whitelist. A malicious site embedding the iframe cannot access tenant data because the server-side validation will reject their domain even if they possess the correct widget key.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →