Creating Custom Element for CORS Bypass | Practical Guide

spyboy's avatarPosted by

Cross-Origin Resource Sharing (CORS) is a security feature implemented by web browsers to restrict how web pages can request resources from a different origin. While this is essential for security, it can sometimes be an obstacle for developers trying to load content from different domains within an iframe. In this blog post, we’ll create a custom HTML element called <x-frame-bypass> that uses proxy servers to bypass CORS restrictions. We’ll dive into the code, explain how it works, and discuss practical use cases.

Code

Here is the complete code for the <x-frame-bypass> custom element:

class XFrameBypass extends HTMLIFrameElement {
    static get observedAttributes() {
        return ['src'];
    }

    constructor() {
        super();
    }

    attributeChangedCallback() {
        this.loadContent(this.src);
    }

    connectedCallback() {
        this.sandbox = this.sandbox || 'allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation';
    }

    loadContent(url, options = {}) {
        if (!url || !url.startsWith('http')) {
            console.error(`Invalid URL: ${url}`);
            return;
        }

        console.log('Loading content:', url);
        this.showLoadingAnimation();

        this.fetchThroughProxy(url, options)
            .then(response => response.text())
            .then(data => this.displayContent(url, data))
            .catch(error => console.error('Failed to load content:', error));
    }

    showLoadingAnimation() {
        this.srcdoc = `
            <html>
                <head>
                    <style>
                        .loader {
                            position: absolute;
                            top: 50%;
                            left: 50%;
                            width: 50px;
                            height: 50px;
                            background-color: #333;
                            border-radius: 50%;
                            transform: translate(-50%, -50%);
                            animation: loader 1s infinite ease-in-out;
                        }
                        @keyframes loader {
                            0% {
                                transform: scale(0);
                            }
                            100% {
                                transform: scale(1);
                                opacity: 0;
                            }
                        }
                    </style>
                </head>
                <body>
                    <div class="loader"></div>
                </body>
            </html>`;
    }

    displayContent(baseURL, data) {
        if (!data) return;

        this.srcdoc = data.replace(
            /<head([^>]*)>/i,
            `<head$1>
                <base href="${baseURL}">
                <script>
                    document.addEventListener('click', event => {
                        if (frameElement && document.activeElement && document.activeElement.href) {
                            event.preventDefault();
                            frameElement.loadContent(document.activeElement.href);
                        }
                    });

                    document.addEventListener('submit', event => {
                        if (frameElement && document.activeElement && document.activeElement.form && document.activeElement.form.action) {
                            event.preventDefault();
                            const form = document.activeElement.form;
                            if (form.method.toLowerCase() === 'post') {
                                frameElement.loadContent(form.action, { method: 'post', body: new FormData(form) });
                            } else {
                                frameElement.loadContent(form.action + '?' + new URLSearchParams(new FormData(form)));
                            }
                        }
                    });
                </script>`
        );
    }

    fetchThroughProxy(url, options, proxyIndex = 0) {
        const proxies = options.proxies || [
            'https://cors-anywhere.herokuapp.com/',
            'https://yacdn.org/proxy/',
            'https://api.codetabs.com/v1/proxy/?quest='
        ];

        if (proxyIndex >= proxies.length) {
            return Promise.reject(new Error('All proxies failed'));
        }

        return fetch(proxies[proxyIndex] + url, options)
            .then(response => {
                if (!response.ok) {
                    throw new Error(`${response.status} ${response.statusText}`);
                }
                return response;
            })
            .catch(() => this.fetchThroughProxy(url, options, proxyIndex + 1));
    }
}

customElements.define('x-frame-bypass', XFrameBypass, { extends: 'iframe' });

Explanation

Observing Attributes

static get observedAttributes() {
    return ['src'];
}

This method tells the custom element to observe changes to the src attribute, which triggers the attributeChangedCallback method.

Constructor

constructor() {
    super();
}

The constructor calls the parent class’s constructor, setting up the element as an iframe.

Attribute Change Handling

attributeChangedCallback() {
    this.loadContent(this.src);
}

When the src attribute changes, this method calls loadContent to load the new content.

Connected Callback

connectedCallback() {
    this.sandbox = this.sandbox || 'allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation';
}

This method sets a default sandbox attribute if none is provided, allowing various features in the iframe.

Loading Content

loadContent(url, options = {}) {
    if (!url || !url.startsWith('http')) {
        console.error(`Invalid URL: ${url}`);
        return;
    }

    console.log('Loading content:', url);
    this.showLoadingAnimation();

    this.fetchThroughProxy(url, options)
        .then(response => response.text())
        .then(data => this.displayContent(url, data))
        .catch(error => console.error('Failed to load content:', error));
}

This method validates the URL, shows a loading animation, and fetches the content through a proxy.

Showing Loading Animation

showLoadingAnimation() {
    this.srcdoc = `
        <html>
            <head>
                <style>
                    .loader {
                        position: absolute;
                        top: 50%;
                        left: 50%;
                        width: 50px;
                        height: 50px;
                        background-color: #333;
                        border-radius: 50%;
                        transform: translate(-50%, -50%);
                        animation: loader 1s infinite ease-in-out;
                    }
                    @keyframes loader {
                        0% {
                            transform: scale(0);
                        }
                        100% {
                            transform: scale(1);
                            opacity: 0;
                        }
                    }
                </style>
            </head>
            <body>
                <div class="loader"></div>
            </body>
        </html>`;
}

This method sets the iframe’s content to a simple loading animation.

Displaying Content

displayContent(baseURL, data) {
    if (!data) return;

    this.srcdoc = data.replace(
        /<head([^>]*)>/i,
        `<head$1>
            <base href="${baseURL}">
            <script>
                document.addEventListener('click', event => {
                    if (frameElement && document.activeElement && document.activeElement.href) {
                        event.preventDefault();
                        frameElement.loadContent(document.activeElement.href);
                    }
                });

                document.addEventListener('submit', event => {
                    if (frameElement && document.activeElement && document.activeElement.form && document.activeElement.form.action) {
                        event.preventDefault();
                        const form = document.activeElement.form;
                        if (form.method.toLowerCase() === 'post') {
                            frameElement.loadContent(form.action, { method: 'post', body: new FormData(form) });
                        } else {
                            frameElement.loadContent(form.action + '?' + new URLSearchParams(new FormData(form)));
                        }
                    }
                });
            </script>`
    );
}

This method injects the fetched content into the iframe, adding a base URL and event listeners to handle navigation and form submissions.

Fetching Through Proxies

fetchThroughProxy(url, options, proxyIndex = 0) {
    const proxies = options.proxies || [
        'https://cors-anywhere.herokuapp.com/',
        'https://yacdn.org/proxy/',
        'https://api.codetabs.com/v1/proxy/?quest='
    ];

    if (proxyIndex >= proxies.length) {
        return Promise.reject(new Error('All proxies failed'));
    }

    return fetch(proxies[proxyIndex] + url, options)
        .then(response => {
            if (!response.ok) {
                throw new Error(`${response.status} ${response.statusText}`);
            }
            return response;
        })
        .catch(() => this.fetchThroughProxy(url, options, proxyIndex + 1));
}

This method tries to fetch the content through a list of proxy servers, handling failures by trying the next proxy in the list.

Use Cases

The <x-frame-bypass> element can be useful in various scenarios, including:

  • Testing: Loading content from different origins during development and testing without modifying server-side CORS settings.
  • Content Aggregation: Creating dashboards or portals that aggregate content from multiple sources.
  • Educational Purposes: Demonstrating how CORS works and how it can be bypassed in a controlled environment.

How to Use

  1. Define the Element: Include the JavaScript code in your project.
  2. Use the Element: Add the custom element to your HTML, specifying the src attribute.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">


 <title>X-Frame-Bypass Example</title>
    <script src="x-frame-bypass.js" defer></script>
</head>
<body>
    <iframe is="x-frame-bypass" src="https://example.com"></iframe>
</body>
</html>

This example demonstrates how to use the <x-frame-bypass> element to load content from a different origin.

Conclusion

The <x-frame-bypass> custom element provides a way to load cross-origin content within an iframe by using proxy servers to bypass CORS restrictions. While this can be useful in certain scenarios, it’s important to use this approach responsibly and be aware of the associated security and ethical considerations. By understanding and leveraging this technique, developers can create more versatile and robust web applications.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.