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:
- The external page includes the snippet generated by
WidgetConfig#iframe_embed_code. - The browser requests
https://<widget-host>/widget/<widget_key>viaWidgetsController#iframe, which rendersapp/views/layouts/widget.html.erb. - The widget's JavaScript makes API calls to
/api_public/v1/widgets/:widget_key/.... - Each request passes through
ApiPublic::V1::WidgetsController#validate_origin, which extracts the caller's domain and runsWidgetConfig#domain_allowed?. - 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.comandwww.example.comsub.example.comand any subdomain of it (wildcard)www.another.com
Summary
- Server-side validation in
ApiPublic::V1::WidgetsController#validate_originprevents 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: ALLOWALLenables broad embedding. - Empty whitelist mode allows public access when
allowed_domainsis 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →