# Widget Security with Iframe Embedding and Domain Restrictions in PropertyWebBuilder

> Secure your widgets with PropertyWebBuilder's iframe embedding and domain restrictions. Ensure only whitelisted domains serve your content for enhanced security.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: security
- Published: 2026-03-01

---

**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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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.

```ruby

# 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:

```html
<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:

```ruby
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`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/widgets_controller.rb) (lines 39-45) sets permissive headers to allow cross-origin requests while maintaining security through origin validation:

```ruby
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:

```ruby
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.