From 4b39ad7167d57efb49e33b5e30b87b4ca26944aa Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Fri, 2 Dec 2022 15:42:01 +0000 Subject: [PATCH] [FIX] website: prevent iframe infinite loop with naked domain/https If the website domain is wrongly configured (set to a URL that will actually be redirected through DNS/proxy/cloudflare etc), the website preview iframe will loop forever. The wrong configuration this commit is about are: - Having set the website domain to naked URL while you configured your naked URL to redirect to your `www` URL. - The other way around (`www` to naked URL). - Having set the website domain to `http` while you configured your `http` domain to redirect to your `https` domain. - The other way around (`https` to `http`) if it's even possible/done by someone While this could just be considered as a misconfiguration, it's a low effort to support it. Also, the bug it implies is quite critical as the user can't access his website app anymore (if he is smart enough and somehow figure it's because of the website domain, he will think about going to the General Settings to remove the domain). All in all, it's a low effort to fix a (rare) critical issue. Step to reproduce (`https`): - Go to runbot (in `https`, normal way) eg: https://21544921-16-0-all.runbot172.odoo.com/ - Edit the website domain and set it to the `http` version eg: http://21544921-16-0-all.runbot172.odoo.com/ - Try to access the website app, it will loop forever, indeed: 1. You are on the `https` URL in your browser tab 2. The website app will try to load the iframe with the website domain as URL, which is the `http` version. 3. The code in charge of loading the iframe URL (website_preview.js) will detect that `http` URL as originating from a different domain than the one of the current tab URL (parent window). 4. In this case, that code will reject that URL and instead load it in the parent window URL -> It will basically redirect your whole browser tab to that URL. This is done as if you are loading the URL of a different website, we want you to be completely redirect to this other website domain. 5. As `https` to `http` redirection (and naked -> www or www -> naked) is done at a lower level (through nginx, dns, cloudflare etc), the browser will not load the `http` requested URL but redirect and load the `https` one. 6. Now, you are basically back to step 1. and entering an infinite loop. The same issue can be reproduced with naked domain and `www` domain but not on runbot and difficult in local. You can also reproduce it in local, but it will just do the useless redirect once and won't loop as nothing is redirecting the naked to www in local: - Edit your `/etc/hosts` file and write ``` 127.0.0.1 rde.com 127.0.0.1 www.rde.com ``` - Edit your website domain to `http://www.rde.com` - Now go to `rde.com:8069` - It will wrongly redirect to `www` version ------------- On top of that, the domain misconfiguration would also lead to useless reload when using the website switcher (it would reload the whole page instead of just the iframe). Step to reproduce on local (on runbot it will be replicable but it will then enter the infinite loop described above): - (Still with the `/etc/hosts` tweak described above) - Edit the website_1 domain and set it to the `www` version eg: `http://www.rde.com:8069/` - Edit the website_2 domain and set it to the `naked` version eg: `http://rde.com:8069/` - Open the website app, land on one of the website (and its URL) - From there, select the other one, it will redirect the whole page to the other domain (naked vs www) instead of just reloading the iframe. ------------- opw-3079168 closes odoo/odoo#107394 X-original-commit: 0808659dd29db938bb6f8386ee2027eafecb2f0f Signed-off-by: Quentin Smetz (qsm) Signed-off-by: Romain Derie (rde) --- .../website_preview/website_preview.js | 12 +++++++++- addons/website/static/src/js/utils.js | 23 +++++++++++++++++++ .../src/systray_items/website_switcher.js | 3 ++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/addons/website/static/src/client_actions/website_preview/website_preview.js b/addons/website/static/src/client_actions/website_preview/website_preview.js index aae14db8e00..85329f8d61c 100644 --- a/addons/website/static/src/client_actions/website_preview/website_preview.js +++ b/addons/website/static/src/client_actions/website_preview/website_preview.js @@ -10,6 +10,7 @@ import { unslugHtmlDataObject } from '../../services/website_service'; import {OptimizeSEODialog} from '@website/components/dialog/seo'; import { routeToUrl } from "@web/core/browser/router_service"; import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service"; +import wUtils from 'website.utils'; const { Component, onWillStart, onMounted, onWillUnmount, useRef, useEffect, useState } = owl; @@ -53,7 +54,16 @@ export class WebsitePreview extends Component { this.backendWebsiteId = unslugHtmlDataObject(backendWebsiteRepr).id; const encodedPath = encodeURIComponent(this.path); - if (this.websiteDomain && this.websiteDomain !== window.location.origin) { + if (this.websiteDomain && !wUtils.isHTTPSorNakedDomainRedirection(this.websiteDomain, window.location.origin)) { + // The website domain might be the naked one while the naked one + // is actually redirecting to `www` (or the other way around). + // In such a case, we need to consider those 2 from the same + // domain and let the iframe load that "different" domain. The + // iframe will actually redirect to the correct one (naked/www), + // which will ends up with the same domain as the parent window + // URL (event if it wasn't, it wouldn't be an issue as those are + // really considered as the same domain, the user will share the + // same session and CORS errors won't be a thing in such a case) window.location.href = `${this.websiteDomain}/web#action=website.website_preview&path=${encodedPath}&website_id=${this.websiteId}`; } else { this.initialUrl = `/website/force/${this.websiteId}?path=${encodedPath}`; diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js index 75cef2c55c2..294c57dacd7 100644 --- a/addons/website/static/src/js/utils.js +++ b/addons/website/static/src/js/utils.js @@ -239,6 +239,28 @@ function websiteDomain(self) { return ['|', ['website_id', '=', false], ['website_id', '=', websiteID]]; } +/** + * Checks if the 2 given URLs are the same, to prevent redirecting uselessly + * from one to another. + * It will consider naked URL and `www` URL as the same URL. + * It will consider `https` URL `http` URL as the same URL. + * + * @param {string} url1 + * @param {string} url2 + * @returns {Boolean} + */ +function isHTTPSorNakedDomainRedirection(url1, url2) { + try { + url1 = new URL(url1).host; + url2 = new URL(url2).host; + } catch { + // Incorrect URL, `false` URL.. + return false; + } + return url1 === url2 || + url1.replace(/^www\./, '') === url2.replace(/^www\./, ''); +} + function sendRequest(route, params) { function _addInput(form, name, value) { let param = document.createElement('input'); @@ -389,6 +411,7 @@ return { prompt: prompt, sendRequest: sendRequest, websiteDomain: websiteDomain, + isHTTPSorNakedDomainRedirection: isHTTPSorNakedDomainRedirection, svgToPNG: svgToPNG, generateGMapIframe: generateGMapIframe, generateGMapLink: generateGMapLink, diff --git a/addons/website/static/src/systray_items/website_switcher.js b/addons/website/static/src/systray_items/website_switcher.js index 4da8f3f7493..3283c26f263 100644 --- a/addons/website/static/src/systray_items/website_switcher.js +++ b/addons/website/static/src/systray_items/website_switcher.js @@ -4,6 +4,7 @@ import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { Dropdown } from "@web/core/dropdown/dropdown"; import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import wUtils from 'website.utils'; const { Component } = owl; @@ -17,7 +18,7 @@ export class WebsiteSwitcherSystray extends Component { name: website.name, id: website.id, callback: () => { - if (website.domain && website.domain !== window.location.origin) { + if (website.domain && !wUtils.isHTTPSorNakedDomainRedirection(website.domain, window.location.origin)) { const { location: { pathname, search, hash } } = this.websiteService.contentWindow; const path = pathname + search + hash; window.location.href = `${website.domain}/web#action=website.website_preview&path=${encodeURI(path)}&website_id=${website.id}`;