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
- Define the Element: Include the JavaScript code in your project.
- Use the Element: Add the custom element to your HTML, specifying the
srcattribute.
<!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.
