merge prs
This commit is contained in:
parent
7888439420
commit
d043b11516
26 changed files with 259 additions and 139 deletions
|
@ -1,191 +0,0 @@
|
|||
# Popups and window methods
|
||||
|
||||
A popup window is one of the oldest methods to show additional document to user.
|
||||
|
||||
Basically, you just run:
|
||||
```js
|
||||
window.open('http://javascript.info/')
|
||||
```
|
||||
|
||||
... And it will open a new window with given URL. Most modern browsers are configured to open new tabs instead of separate windows.
|
||||
|
||||
## Popup blocking
|
||||
|
||||
Popups exist from really ancient times. The initial idea was to show another content without closing the main window. As of now, there are other ways to do that: JavaScript is able to send requests for server, so popups are rarely used. But sometimes they are still handy.
|
||||
|
||||
In the past evil sites abused popups a lot. A bad page could open tons of popup windows with ads. So now most browsers try to block popups and protect the user.
|
||||
|
||||
**Most browsers block popups if they are called outside of user-triggered event handlers like `onclick`.**
|
||||
|
||||
If you think about it, that's a bit tricky. If the code is directly in an `onclick` handler, then that's easy. But what is the popup opens in `setTimeout`?
|
||||
|
||||
Try this code:
|
||||
|
||||
```js run
|
||||
// open after 3 seconds
|
||||
setTimeout(() => window.open('http://google.com'), 3000);
|
||||
```
|
||||
|
||||
The popup opens in Chrome, but gets blocked in Firefox.
|
||||
|
||||
...And this works in Firefox too:
|
||||
|
||||
```js run
|
||||
// open after 1 seconds
|
||||
setTimeout(() => window.open('http://google.com'), 1000);
|
||||
```
|
||||
|
||||
The difference is that Firefox treats a timeout of 2000ms or less are acceptable, but after it -- removes the "trust", assuming that now it's "outside of the user action". So the first one is blocked, and the second one is not.
|
||||
|
||||
## Modern usage
|
||||
|
||||
As of now, we have many methods to load and show data on-page with JavaScript. But there are still situations when a popup works best.
|
||||
|
||||
For instance, many shops use online chats for consulting people. A visitor clicks on the button, it runs `window.open` and opens the popup with the chat.
|
||||
|
||||
Why a popup is good here, why not in-page?
|
||||
|
||||
1. A popup is a separate window with its own independent JavaScript environment. So a chat service doesn't need to integrate with scripts of the main shop site.
|
||||
2. A popup is very simple to attach to a site, little to no overhead. It's only a small button, without additional scripts.
|
||||
3. A popup may persist even if the user left the page. For example, a consult advices the user to visit the page of a new "Super-Cooler" goodie. The user goes there in the main window without leaving the chat.
|
||||
|
||||
## window.open
|
||||
|
||||
The syntax to open a popup is: `window.open(url, name, params)`:
|
||||
|
||||
url
|
||||
: An URL to load into the new window.
|
||||
|
||||
name
|
||||
: A name of the new window. Each window has a `window.name`, and here we can specify which window to use for the popup. If there's already a window with such name -- the given URL opens in it, otherwise a new window is opened.
|
||||
|
||||
params
|
||||
: The configuration string for the new window. It contains settings, delimited by a comma. There must be no spaces in params, for instance: `width:200,height=100`.
|
||||
|
||||
Settings for `params`:
|
||||
|
||||
- Position:
|
||||
- `left/top` (numeric) -- coordinates of the window top-left corner on the screen. There is a limitation: a new window cannot be positioned offscreen.
|
||||
- `width/height` (numeric) -- width and height of a new window. There is a limit on minimal width/height, so it's impossible to create an invisible window.
|
||||
- Window features:
|
||||
- `menubar` (yes/no) -- shows or hides the browser menu on the new window.
|
||||
- `toolbar` (yes/no) -- shows or hides the browser navigation bar (back, forward, reload etc) on the new window.
|
||||
- `location` (yes/no) -- shows or hides the URL field in the new window. FF and IE don't allow to hide it by default.
|
||||
- `status` (yes/no) -- shows or hides the status bar. Again, most browsers force it to show.
|
||||
- `resizable` (yes/no) -- allows to disable the resize for the new window. Not recommended.
|
||||
- `scrollbars` (yes/no) -- allows to disable the scrollbars for the new window. Not recommended.
|
||||
|
||||
|
||||
There is also a number of less supported browser-specific features, which are usually not used. Check <a href="https://developer.mozilla.org/en/DOM/window.open">window.open in MDN</a> for examples.
|
||||
|
||||
## Example: a minimalistic window
|
||||
|
||||
Let's open a window with minimal set of features just to see which of them browser allows to disable:
|
||||
|
||||
```js run
|
||||
let params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
|
||||
width=0,height=0,left=-1000,top=-1000`;
|
||||
|
||||
open('/', 'test', params);
|
||||
```
|
||||
|
||||
Here most "window features" are disabled and window is positioned offscreen. Run it and see what really happens. Most browsers "fix" odd things like zero `width/height` and offscreen `left/top`. For instance, Chrome open such a window with full width/height, so that it occupies the full screen.
|
||||
|
||||
Let's add normal positioning options and reasonable `width`, `height`, `left`, `top` coordinates:
|
||||
|
||||
```js run
|
||||
let params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
|
||||
width=600,height=300,left=100,top=100`;
|
||||
|
||||
open('/', 'test', params);
|
||||
```
|
||||
|
||||
Most browsers show the example above as required.
|
||||
|
||||
Rules for omitted settings:
|
||||
|
||||
- If there is no 3rd argument in the `open` call, or it is empty, then the default window parameters are used.
|
||||
- If there is a string of params, but some yes/no features are omitted, then the omitted features are disabled, if the browser allows that. So if you specify params, make sure you explicitly set all required features to yes.
|
||||
- If there is no `left/top` in params, then the browser tries to open a new window near the last opened window.
|
||||
- If there is no `width/height`, then the new window will be the same size as the last opened.
|
||||
|
||||
## Accessing a popup
|
||||
|
||||
The `open` call returns a reference to the new window. It can be used to manipulate it's properties, change location and even more.
|
||||
|
||||
In the example below, the contents of the new window is modified after loading.
|
||||
|
||||
```js run
|
||||
let newWindow = open('/', 'example', 'width=300,height=300')
|
||||
newWindow.focus();
|
||||
|
||||
newWindow.onload = function() {
|
||||
let html = `<div style="font-size:30px">Welcome!</div>`;
|
||||
*!*
|
||||
newWindow.document.body.insertAdjacentHTML('afterbegin', html);
|
||||
*/!*
|
||||
};
|
||||
```
|
||||
|
||||
Please note that external `document` content is only accessible for windows from the same origin (the same protocol://domain:port).
|
||||
|
||||
For windows with URLs from another sites, we are able to change the location by assigning `newWindow.location=...`, but we can't read the location or access the content. That's for user safety, so that an evil page can't open a popup with `http://gmail.com` and read the data. We'll talk more about it later.
|
||||
|
||||
## Accessing the opener window
|
||||
|
||||
A popup may access the "opener" window as well. A JavaScript in it may use `window.opener` to access the window that opened it. It is `null` for all windows except popups.
|
||||
|
||||
So both the main window and the popup have a reference to each other. They may modify each other freely assuming that they come from the same origin. If that's not so, then there are still means to communicate, to be covered in the next chapter <info:cross-window-communication>.
|
||||
|
||||
## Closing a popup
|
||||
|
||||
If we don't need a popup any more, we can call `newWindow.close()` on it.
|
||||
|
||||
Technically, the `close()` method is available for any `window`, but `window.close()` is ignored by most browsers if `window` is not created with `window.open()`.
|
||||
|
||||
The `newWindow.closed` is `true` if the window is closed. That's useful to check if the popup (or the main window) is still open or not. A user could close it, and our code should take that possibility into account.
|
||||
|
||||
This code loads and then closes the window:
|
||||
|
||||
```js run
|
||||
let newWindow = open('/', 'example', 'width=300,height=300')
|
||||
newWindow.onload = function() {
|
||||
newWindow.close();
|
||||
alert(newWindow.closed); // true
|
||||
};
|
||||
```
|
||||
|
||||
## Focus/blur on a popup
|
||||
|
||||
Theoretically, there are `window.focus()` and `window.blur()` methods to focus/unfocus on a window. Also there are `focus/blur` events that allow to focus a window and catch the moment when the visitor switches elsewhere.
|
||||
|
||||
In the past evil pages abused those. For instance, look at this code:
|
||||
|
||||
```js run
|
||||
window.onblur = () => window.focus();
|
||||
```
|
||||
|
||||
When a user attempts to switch out of the window (`blur`), it brings it back to focus. The intention is to "lock" the user within the `window`.
|
||||
|
||||
So, there are limitations that forbid the code like that. There are many limitations to protect the user from ads and evils pages. They depend on the browser.
|
||||
|
||||
For instance, a mobile browser usually ignores that call completely. Also focusing doesn't work when a popup opens in a separate tab rather than a new window.
|
||||
|
||||
Still, there are some things that can be done.
|
||||
|
||||
For instance:
|
||||
|
||||
- When we open a popup, it's might be a good idea to run a `newWindow.focus()` on it. Just in case, for some OS/browser combinations it ensures that the user is in the new window now.
|
||||
- If we want to track when a visitor actually uses our web-app, we can track `window.onfocus/onblur`. That allows us to suspend/resume in-page activities, animations etc. But please note that the `blur` event means that the visitor switched out from the window, but they still may observe it. The window is in the background, but still may be visible.
|
||||
|
||||
## Summary
|
||||
|
||||
- A popup can be opened by the `open(url, name, params)` call. It returns the reference to the newly opened window.
|
||||
- By default, browsers block `open` calls from the code outside of user actions. Usually a notification appears, so that a user may allow them.
|
||||
- The popup may access the opener window using the `window.opener` property, so the two are connected.
|
||||
- If the main window and the popup come from the same origin, they can freely read and modify each other. Otherwise, they can change location of each other and communicate using messages (to be covered).
|
||||
- To close the popup: use `close()` call. Also the user may close them (just like any other windows). The `window.closed` is `true` after that.
|
||||
- Methods `focus()` and `blur()` allow to focus/unfocus a window. Sometimes.
|
||||
- Events `focus` and `blur` allow to track switching in and out of the window. But please note that a window may still be visible even in the background state, after `blur`.
|
||||
|
||||
Also if we open a popup, a good practice is to notify the user about it. An icon with the opening window can help the visitor to survive the focus shift and keep both windows in mind.
|
|
@ -1,367 +0,0 @@
|
|||
# Cross-window communication
|
||||
|
||||
The "Same Origin" (same site) policy limits access of windows and frames to each other.
|
||||
|
||||
The idea is that if a user has two pages open: one from `john-smith.com`, and another one is `gmail.com`, then they wouldn't want a script from `john-smith.com` to read our mail from `gmail.com`. So, the purpose of the "Same Origin" policy is to protect users from information theft.
|
||||
|
||||
## Same Origin [#same-origin]
|
||||
|
||||
Two URLs are said to have the "same origin" if they have the same protocol, domain and port.
|
||||
|
||||
These URLs all share the same origin:
|
||||
|
||||
- `http://site.com`
|
||||
- `http://site.com/`
|
||||
- `http://site.com/my/page.html`
|
||||
|
||||
These ones do not:
|
||||
|
||||
- <code>http://<b>www.</b>site.com</code> (another domain: `www.` matters)
|
||||
- <code>http://<b>site.org</b></code> (another domain: `.org` matters)
|
||||
- <code><b>https://</b>site.com</code> (another protocol: `https`)
|
||||
- <code>http://site.com:<b>8080</b></code> (another port: `8080`)
|
||||
|
||||
The "Same Origin" policy states that:
|
||||
|
||||
- if we have a reference to another window, e.g. a popup created by `window.open` or a window inside `<iframe>`, and that window comes from the same origin, then we have full access to that window.
|
||||
- otherwise, if it comes from another origin, then we can't access the content of that window: variables, document, anything. The only exception is `location`: we can change it (thus redirecting the user). But we can't not *read* location (so we can't see where the user is now, no information leak).
|
||||
|
||||
Now let's see some examples. First, we'll look at pages that come from the same origin and do not conflict with the "Same Origin" policy, and afterwards we'll cover cross-window messaging that allows to work around the "Same Origin" policy.
|
||||
|
||||
|
||||
````warn header="Subdomains may be same-origin"
|
||||
There's a small exclusion in the "Same Origin" policy.
|
||||
|
||||
If windows share the same second-level domain, for instance `john.site.com`, `peter.site.com` and `site.com` (so that their common second-level domain is `site.com`), they can be treated as coming from the "same origin".
|
||||
|
||||
To make it work, all such pages (including the one from `site.com`) should run the code:
|
||||
|
||||
```js
|
||||
document.domain = 'site.com';
|
||||
```
|
||||
|
||||
That's all. Now they can interact without limitations. Again, that's only possible for pages with the same second-level domain.
|
||||
````
|
||||
|
||||
## Accessing an iframe contents
|
||||
|
||||
Our first example covers iframes. An `<iframe>` is a two-faced beast. From one side it's a tag, just like `<script>` or `<img>`. From the other side it's a window-in-window.
|
||||
|
||||
The embedded window has a separate `document` and `window` objects.
|
||||
|
||||
We can access them like using the properties:
|
||||
|
||||
- `iframe.contentWindow` is a reference to the window inside the `<iframe>`.
|
||||
- `iframe.contentDocument` is a reference to the document inside the `<iframe>`.
|
||||
|
||||
When we access an embedded window, the browser checks if the iframe has the same origin. If that's not so then the access is denied (with exclusions noted above).
|
||||
|
||||
For instance, here's an `<iframe>` from another origin:
|
||||
|
||||
```html run
|
||||
<iframe src="https://example.com" id="iframe"></iframe>
|
||||
|
||||
<script>
|
||||
iframe.onload = function() {
|
||||
// we can get the reference to the inner window
|
||||
let iframeWindow = iframe.contentWindow;
|
||||
|
||||
try {
|
||||
// ...but not to the document inside it
|
||||
let doc = iframe.contentDocument;
|
||||
} catch(e) {
|
||||
alert(e); // Security Error (another origin)
|
||||
}
|
||||
|
||||
// also we can't read the URL of the page in it
|
||||
try {
|
||||
alert(iframe.contentWindow.location);
|
||||
} catch(e) {
|
||||
alert(e); // Security Error
|
||||
}
|
||||
|
||||
// ...but we can change it (and thus load something else into the iframe)!
|
||||
iframe.contentWindow.location = '/'; // works
|
||||
|
||||
iframe.onload = null; // clear the handler, to run this code only once
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
The code above shows errors for any operations except:
|
||||
|
||||
- Getting the reference to the inner window `iframe.contentWindow`
|
||||
- Changing its `location`.
|
||||
|
||||
```smart header="`iframe.onload` vs `iframe.contentWindow.onload`"
|
||||
The `iframe.onload` event is actually the same as `iframe.contentWindow.onload`. It triggers when the embedded window fully loads with all resources.
|
||||
|
||||
...But `iframe.onload` is always available, while `iframe.contentWindow.onload` needs the same origin.
|
||||
```
|
||||
|
||||
And now an example with the same origin. We can do anything with the embedded window:
|
||||
|
||||
```html run
|
||||
<iframe src="/" id="iframe"></iframe>
|
||||
|
||||
<script>
|
||||
iframe.onload = function() {
|
||||
// just do anything
|
||||
iframe.contentDocument.body.prepend("Hello, world!");
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### Please wait until the iframe loads
|
||||
|
||||
When an iframe is created, it immediately has a document. But that document is different from the one that finally loads into it!
|
||||
|
||||
Here, look:
|
||||
|
||||
|
||||
```html run
|
||||
<iframe src="/" id="iframe"></iframe>
|
||||
|
||||
<script>
|
||||
let oldDoc = iframe.contentDocument;
|
||||
iframe.onload = function() {
|
||||
let newDoc = iframe.contentDocument;
|
||||
*!*
|
||||
// the loaded document is not the same as initial!
|
||||
alert(oldDoc == newDoc); // false
|
||||
*/!*
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
That's actually a well-known pitfall for developers. We shouldn't work with the document immediately, because that's the *wrong document*. If we set any event handlers on it, they will be ignored.
|
||||
|
||||
...But the `onload` event triggers when the whole iframe with all resources is loaded. What if we want to act sooner, on `DOMContentLoaded` of the embedded document?
|
||||
|
||||
That's not possible if the iframe comes from another origin. But for the same origin we can try to catch the moment when a new document appears, and then setup necessary handlers, like this:
|
||||
|
||||
```html run
|
||||
<iframe src="/" id="iframe"></iframe>
|
||||
|
||||
<script>
|
||||
let oldDoc = iframe.contentDocument;
|
||||
|
||||
// every 100 ms check if the document is the new one
|
||||
let timer = setInterval(() => {
|
||||
if (iframe.contentDocument == oldDoc) return;
|
||||
|
||||
// new document, let's set handlers
|
||||
iframe.contentDocument.addEventListener('DOMContentLoaded', () => {
|
||||
iframe.contentDocument.body.prepend('Hello, world!');
|
||||
});
|
||||
|
||||
clearInterval(timer); // cancel setInterval, don't need it any more
|
||||
}, 100);
|
||||
</script>
|
||||
```
|
||||
|
||||
Let me know in comments if you know a better solution here.
|
||||
|
||||
## window.frames
|
||||
|
||||
An alternative way to get a window object for `<iframe>` -- is to get it from the named collection `window.frames`:
|
||||
|
||||
- By number: `window.frames[0]` -- the window object for the first frame in the document.
|
||||
- By name: `window.frames.iframeName` -- the window object for the frame with `name="iframeName"`.
|
||||
|
||||
For instance:
|
||||
|
||||
```html run
|
||||
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
|
||||
|
||||
<script>
|
||||
alert(iframe.contentWindow == frames[0]); // true
|
||||
alert(iframe.contentWindow == frames.win); // true
|
||||
</script>
|
||||
```
|
||||
|
||||
An iframe may have other iframes inside. The corresponding `window` objects form a hierarchy.
|
||||
|
||||
Navigation links are:
|
||||
|
||||
- `window.frames` -- the collection of "children" windows (for nested frames).
|
||||
- `window.parent` -- the reference to the "parent" (outer) window.
|
||||
- `window.top` -- the reference to the topmost parent window.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
window.frames[0].parent === window; // true
|
||||
```
|
||||
|
||||
We can use the `top` property to check if the current document is open inside a frame or not:
|
||||
|
||||
```js run
|
||||
if (window == top) { // current window == window.top?
|
||||
alert('The script is in the topmost window, not in a frame');
|
||||
} else {
|
||||
alert('The script runs in a frame!');
|
||||
}
|
||||
```
|
||||
|
||||
## The sandbox attribute
|
||||
|
||||
The `sandbox` attribute allows for the exclusion of certain actions inside an `<iframe>` in order to prevent it executing untrusted code. It "sandboxes" the iframe by treating it as coming from another origin and/or applying other limitations.
|
||||
|
||||
By default, for `<iframe sandbox src="...">` the "default set" of restrictions is applied to the iframe. But we can provide a space-separated list of "excluded" limitations as a value of the attribute, like this: `<iframe sandbox="allow-forms allow-popups">`. The listed limitations are not applied.
|
||||
|
||||
In other words, an empty `"sandbox"` attribute puts the strictest limitations possible, but we can put a space-delimited list of those that we want to lift.
|
||||
|
||||
Here's a list of limitations:
|
||||
|
||||
`allow-same-origin`
|
||||
: By default `"sandbox"` forces the "different origin" policy for the iframe. In other words, it makes the browser to treat the `iframe` as coming from another origin, even if its `src` points to the same site. With all implied restrictions for scripts. This option removes that feature.
|
||||
|
||||
`allow-top-navigation`
|
||||
: Allows the `iframe` to change `parent.location`.
|
||||
|
||||
`allow-forms`
|
||||
: Allows to submit forms from `iframe`.
|
||||
|
||||
`allow-scripts`
|
||||
: Allows to run scripts from the `iframe`.
|
||||
|
||||
`allow-popups`
|
||||
: Allows to `window.open` popups from the `iframe`
|
||||
|
||||
See [the manual](mdn:/HTML/Element/iframe) for more.
|
||||
|
||||
The example below demonstrates a sandboxed iframe with the default set of restrictions: `<iframe sandbox src="...">`. It has some JavaScript and a form.
|
||||
|
||||
Please note that nothing works. So the default set is really harsh:
|
||||
|
||||
[codetabs src="sandbox" height=140]
|
||||
|
||||
|
||||
```smart
|
||||
The purpose of the `"sandbox"` attribute is only to *add more* restrictions. It cannot remove them. In particular, it can't relax same-origin restrictions if the iframe comes from another origin.
|
||||
```
|
||||
|
||||
## Cross-window messaging
|
||||
|
||||
The `postMessage` interface allows windows to talk to each other no matter which origin they are from.
|
||||
|
||||
So, it's a way around the "Same Origin" policy. It allows a window from `john-smith.com` to talk to `gmail.com` and exchange information, but only if they both agree and call corresponding Javascript functions. That makes it safe for users.
|
||||
|
||||
The interface has two parts.
|
||||
|
||||
### postMessage
|
||||
|
||||
The window that wants to send a message calls [postMessage](mdn:api/Window.postMessage) method of the receiving window. In other words, if we want to send the message to `win`, we should call `win.postMessage(data, targetOrigin)`.
|
||||
|
||||
Arguments:
|
||||
|
||||
`data`
|
||||
: The data to send. Can be any object, the data is cloned using the "structured cloning algorithm". IE supports only strings, so we should `JSON.stringify` complex objects to support that browser.
|
||||
|
||||
`targetOrigin`
|
||||
: Specifies the origin for the target window, so that only a window from the given origin will get the message.
|
||||
|
||||
The `targetOrigin` is a safety measure. Remember, if the target window comes from another origin, we can't read it's `location`. So we can't be sure which site is open in the intended window right now: the user could navigate away.
|
||||
|
||||
Specifying `targetOrigin` ensures that the window only receives the data if it's still at that site. Good when the data is sensitive.
|
||||
|
||||
For instance, here `win` will only receive the message if it has a document from the origin `http://example.com`:
|
||||
|
||||
```html no-beautify
|
||||
<iframe src="http://example.com" name="example">
|
||||
|
||||
<script>
|
||||
let win = window.frames.example;
|
||||
|
||||
win.postMessage("message", "http://example.com");
|
||||
</script>
|
||||
```
|
||||
|
||||
If we don't want that check, we can set `targetOrigin` to `*`.
|
||||
|
||||
```html no-beautify
|
||||
<iframe src="http://example.com" name="example">
|
||||
|
||||
<script>
|
||||
let win = window.frames.example;
|
||||
|
||||
*!*
|
||||
win.postMessage("message", "*");
|
||||
*/!*
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
### onmessage
|
||||
|
||||
To receive a message, the target window should have a handler on the `message` event. It triggers when `postMessage` is called (and `targetOrigin` check is successful).
|
||||
|
||||
The event object has special properties:
|
||||
|
||||
`data`
|
||||
: The data from `postMessage`.
|
||||
|
||||
`origin`
|
||||
: The origin of the sender, for instance `http://javascript.info`.
|
||||
|
||||
`source`
|
||||
: The reference to the sender window. We can immediately `postMessage` back if we want.
|
||||
|
||||
To assign that handler, we should use `addEventListener`, a short syntax `window.onmessage` does not work.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```js
|
||||
window.addEventListener("message", function(event) {
|
||||
if (event.origin != 'http://javascript.info') {
|
||||
// something from an unknown domain, let's ignore it
|
||||
return;
|
||||
}
|
||||
|
||||
alert( "received: " + event.data );
|
||||
});
|
||||
```
|
||||
|
||||
The full example:
|
||||
|
||||
[codetabs src="postmessage" height=120]
|
||||
|
||||
```smart header="There's no delay"
|
||||
There's totally no delay between `postMessage` and the `message` event. That happens synchronously, even faster than `setTimeout(...,0)`.
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
To call methods and access the content of another window, we should first have a reference to it.
|
||||
|
||||
For popups we have two properties:
|
||||
- `window.open` -- opens a new window and returns a reference to it,
|
||||
- `window.opener` -- a reference to the opener window from a popup
|
||||
|
||||
For iframes, we can access parent/children windows using:
|
||||
- `window.frames` -- a collection of nested window objects,
|
||||
- `window.parent`, `window.top` are the references to parent and top windows,
|
||||
- `iframe.contentWindow` is the window inside an `<iframe>` tag.
|
||||
|
||||
If windows share the same origin (host, port, protocol), then windows can do whatever they want with each other.
|
||||
|
||||
Otherwise, only possible actions are:
|
||||
- Change the location of another window (write-only access).
|
||||
- Post a message to it.
|
||||
|
||||
|
||||
Exclusions are:
|
||||
- Windows that share the same second-level domain: `a.site.com` and `b.site.com`. Then setting `document.domain='site.com'` in both of them puts them into the "same origin" state.
|
||||
- If an iframe has a `sandbox` attribute, it is forcefully put into the "different origin" state, unless the `allow-same-origin` is specified in the attribute value. That can be used to run untrusted code in iframes from the same site.
|
||||
|
||||
The `postMessage` interface allows two windows to talk with security checks:
|
||||
|
||||
1. The sender calls `targetWin.postMessage(data, targetOrigin)`.
|
||||
2. If `targetOrigin` is not `'*'`, then the browser checks if window `targetWin` has the URL from `targetWin` site.
|
||||
3. If it is so, then `targetWin` triggers the `message` event with special properties:
|
||||
- `origin` -- the origin of the sender window (like `http://my.site.com`)
|
||||
- `source` -- the reference to the sender window.
|
||||
- `data` -- the data, any object in everywhere except IE that supports only strings.
|
||||
|
||||
We should use `addEventListener` to set the handler for this event inside the target window.
|
|
@ -1,18 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
Receiving iframe.
|
||||
<script>
|
||||
window.addEventListener('message', function(event) {
|
||||
alert(`Received ${event.data} from ${event.origin}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,25 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<form id="form">
|
||||
<input type="text" placeholder="Enter message" name="message">
|
||||
<input type="submit" value="Click to send">
|
||||
</form>
|
||||
|
||||
<iframe src="iframe.html" id="iframe" style="display:block;height:60px"></iframe>
|
||||
|
||||
<script>
|
||||
form.onsubmit = function() {
|
||||
iframe.contentWindow.postMessage(this.message.value, '*');
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,15 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>The iframe below is has <code>sandbox</code> attribute.</div>
|
||||
|
||||
<iframe sandbox src="sandboxed.html" style="height:60px;width:90%"></iframe>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,18 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<button onclick="alert(123)">Click to run a script (doesn't work)</button>
|
||||
|
||||
<form action="http://google.com">
|
||||
<input type="text">
|
||||
<input type="submit" value="Submit (doesn't work)">
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,239 +0,0 @@
|
|||
# The clickjacking attack
|
||||
|
||||
The "clickjacking" attack allows an evil page to click on a "victim site" *on behalf of the visitor*.
|
||||
|
||||
Many sites were hacked this way, including Twitter, Facebook, Paypal and other sites. They are all fixed, of course.
|
||||
|
||||
## The idea
|
||||
|
||||
The idea is very simple.
|
||||
|
||||
Here's how clickjacking was done with Facebook:
|
||||
|
||||
1. A visitor is lured to the evil page. It doesn't matter how.
|
||||
2. The page has a harmless-looking link on it (like "get rich now" or "click here, very funny").
|
||||
3. Over that link the evil page positions a transparent `<iframe>` with `src` from facebook.com, in such a way that the "Like" button is right above that link. Usually that's done with `z-index`.
|
||||
4. In attempting to click the link, the visitor in fact clicks the button.
|
||||
|
||||
## The demo
|
||||
|
||||
Here's how the evil page looks. To make things clear, the `<iframe>` is half-transparent (in real evil pages it's fully transparent):
|
||||
|
||||
```html run height=120 no-beautify
|
||||
<style>
|
||||
iframe { /* iframe from the victim site */
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top:0; left:-20px;
|
||||
*!*
|
||||
opacity: 0.5; /* in real opacity:0 */
|
||||
*/!*
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>Click to get rich now:</div>
|
||||
|
||||
<!-- The url from the victim site -->
|
||||
*!*
|
||||
<iframe src="/clickjacking/facebook.html"></iframe>
|
||||
|
||||
<button>Click here!</button>
|
||||
*/!*
|
||||
|
||||
<div>...And you're cool (I'm a cool hacker actually)!</div>
|
||||
```
|
||||
|
||||
The full demo of the attack:
|
||||
|
||||
[codetabs src="clickjacking-visible" height=160]
|
||||
|
||||
Here we have a half-transparent `<iframe src="facebook.html">`, and in the example we can see it hovering over the button. A click on the button actually clicks on the iframe, but that's not visible to the user, because the iframe is transparent.
|
||||
|
||||
As a result, if the visitor is authorized on Facebook ("remember me" is usually turned on), then it adds a "Like". On Twitter that would be a "Follow" button.
|
||||
|
||||
Here's the same example, but closer to reality, with `opacity:0` for `<iframe>`:
|
||||
|
||||
[codetabs src="clickjacking" height=160]
|
||||
|
||||
All we need to attack -- is to position the `<iframe>` on the evil page in such a way that the button is right over the link. That's usually possible with CSS.
|
||||
|
||||
```smart header="Clickjacking is for clicks, not for keyboard"
|
||||
The attack only affects mouse actions.
|
||||
|
||||
Technically, if we have a text field to hack, then we can position an iframe in such a way that text fields overlap each other. So when a visitor tries to focus on the input they see on the page, they actually focus on the input inside the iframe.
|
||||
|
||||
But then there's a problem. Everything that the visitor types will be hidden, because the iframe is not visible.
|
||||
|
||||
People will usually stop typing when they can't see their new characters printing on the screen.
|
||||
```
|
||||
|
||||
## Old-school defences (weak)
|
||||
|
||||
The oldest defence is a bit of JavaScript which forbids opening the page in a frame (so-called "framebusting").
|
||||
|
||||
That looks like this:
|
||||
|
||||
```js
|
||||
if (top != window) {
|
||||
top.location = window.location;
|
||||
}
|
||||
```
|
||||
|
||||
That is: if the window finds out that it's not on top, then it automatically makes itself the top.
|
||||
|
||||
This not a reliable defence, because there are many ways to hack around it. Let's cover a few.
|
||||
|
||||
### Blocking top-navigation
|
||||
|
||||
We can block the transition caused by changing `top.location` in the [beforeunload](info:onload-ondomcontentloaded#window.onbeforeunload) event.
|
||||
|
||||
The top page (belonging to the hacker) sets a handler to it, and when the `iframe` tries to change `top.location` the visitor gets a message asking them whether they want to leave.
|
||||
|
||||
Like this:
|
||||
```js
|
||||
window.onbeforeunload = function() {
|
||||
window.onbeforeunload = null;
|
||||
return "Want to leave without learning all the secrets (he-he)?";
|
||||
};
|
||||
```
|
||||
|
||||
In most cases the visitor would answer negatively because they don't know about the iframe - all they can see is the top page, leading them to think there is no reason to leave. So `top.location` won't change!
|
||||
|
||||
In action:
|
||||
|
||||
[codetabs src="top-location"]
|
||||
|
||||
### Sandbox attribute
|
||||
|
||||
One of the things restricted by the `sandbox` attribute is navigation. A sandboxed iframe may not change `top.location`.
|
||||
|
||||
So we can add the iframe with `sandbox="allow-scripts allow-forms"`. That would relax the restrictions, permitting scripts and forms. But we omit `allow-top-navigation` so that changing `top.location` is forbidden.
|
||||
|
||||
Here's the code:
|
||||
|
||||
```html
|
||||
<iframe *!*sandbox="allow-scripts allow-forms"*/!* src="facebook.html"></iframe>
|
||||
```
|
||||
|
||||
There are other ways to work around that simple protection too.
|
||||
|
||||
## X-Frame-Options
|
||||
|
||||
The server-side header `X-Frame-Options` can permit or forbid displaying the page inside a frame.
|
||||
|
||||
It must be sent *by the server*: the browser will ignore it if found in a `<meta>` tag. So, `<meta http-equiv="X-Frame-Options"...>` won't do anything.
|
||||
|
||||
The header may have 3 values:
|
||||
|
||||
|
||||
`DENY`
|
||||
: Never ever show the page inside a frame.
|
||||
|
||||
`SAMEORIGIN`
|
||||
: Allow inside a frame if the parent document comes from the same origin.
|
||||
|
||||
`ALLOW-FROM domain`
|
||||
: Allow inside a frame if the parent document is from the given domain.
|
||||
|
||||
For instance, Twitter uses `X-Frame-Options: SAMEORIGIN`.
|
||||
|
||||
````online
|
||||
Here's the result:
|
||||
|
||||
```html
|
||||
<iframe src="https://twitter.com"></iframe>
|
||||
```
|
||||
|
||||
<!-- ebook: prerender/ chrome headless dies and timeouts on this iframe -->
|
||||
<iframe src="https://twitter.com"></iframe>
|
||||
|
||||
Depending on your browser, the `iframe` above is either empty or alerting you that the browser won't permit that page to be navigating in this way.
|
||||
````
|
||||
|
||||
## Showing with disabled functionality
|
||||
|
||||
The `X-Frame-Options` header has a side-effect. Other sites won't be able to show our page in a frame, even if they have good reasons to do so.
|
||||
|
||||
So there are other solutions... For instance, we can "cover" the page with a `<div>` with `height: 100%; width: 100%;`, so that it intercepts all clicks. That `<div>` should disappear if `window == top` or if we figure out that we don't need the protection.
|
||||
|
||||
Something like this:
|
||||
|
||||
```html
|
||||
<style>
|
||||
#protector {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 99999999;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="protector">
|
||||
<a href="/" target="_blank">Go to the site</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// there will be an error if top window is from the different origin
|
||||
// but that's ok here
|
||||
if (top.document.domain == document.domain) {
|
||||
protector.remove();
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
The demo:
|
||||
|
||||
[codetabs src="protector"]
|
||||
|
||||
## Samesite cookie attribute
|
||||
|
||||
The `samesite` cookie attribute can also prevent clickjacking attacks. The purpose of the attribute is to prevent cookies from being sent to a website when the user doesn't intend to visit the website. It is designed to prevent cross-site request forgery attacks, but also helps with clickjacking because a hijacked click usually results in an unintended request to a different site. When a cookie has the `samesite` attribute, whether the value is `strict` or `lax`, cookies are not sent to a website when it is loaded inside an iframe.
|
||||
|
||||
The `samesite` attribute can be set using HTTP response headers or JavaScript. Via HTTP, it looks like:
|
||||
|
||||
`Set-Cookie: demoCookie=demoValue; samesite=lax`
|
||||
|
||||
or
|
||||
|
||||
`Set-Cookie: demoCookie=demoValue; samesite=strict`
|
||||
|
||||
In JavaScript, it is:
|
||||
|
||||
```html
|
||||
document.cookie = "demoCookie=demoValue; SameSite=Lax";
|
||||
document.cookie = "demoCookie=demoValue; SameSite=Strict";
|
||||
```
|
||||
|
||||
When the value is `lax`, these types of requests are blocked:
|
||||
- Form POST submit (<form method="POST" action="...">)
|
||||
- iframe (<iframe src="..."></iframe>)
|
||||
- AJAX ($.get("..."))
|
||||
- Image (<img src="...">)
|
||||
- Script (<script src="..."></script>)
|
||||
- Stylesheet (<link rel="stylesheet" type="text/css" href="...">)
|
||||
|
||||
When the value is `strict`, these types of requests are also blocked, in addition to those under `lax`:
|
||||
- Clicking a link (<a href="..."></a>)
|
||||
- Prerender (<link rel="prerender" href=".."/>)
|
||||
- Form GET submit (<form method="GET" action="...">)
|
||||
|
||||
In this case, we are concerned with iframe requests. A clickjacking attempt would fail because the user is not considered logged into, for example, Facebook, so they can't "Like" anything through the iframe.
|
||||
|
||||
The `samesite` attribute will not have an effect when cookies are not used. This may allow websites to easily show public, unauthenticated pages in iframes on unaffiliated websites. However, this may also allow clickjacking attacks to work in a few limited cases. An anonymous polling website that prevents duplicate voting by checking IP addresses, for example, would still be vulnerable to clickjacking because it does not authenticate users using cookies.
|
||||
|
||||
## Summary
|
||||
|
||||
Clickjacking is a way to "trick" users into clicking on a malicious site without even knowing what's happening. That's dangerous if there are important click-activated actions.
|
||||
|
||||
A hacker can post a link to their evil page in a message, or lure visitors to their page by some other means. There are many variations.
|
||||
|
||||
From one perspective -- the attack is "not deep": all a hacker is doing is intercepting a single click. But from another perspective, if the hacker knows that after the click another control will appear, then they may use cunning messages to coerce the user into clicking on them as well.
|
||||
|
||||
The attack is quite dangerous, because when we engineer the UI we usually don't anticipate that a hacker may click on behalf of the visitor. So vulnerabilities can be found in totally unexpected places.
|
||||
|
||||
- It is recommended to use `X-Frame-Options: SAMEORIGIN` on pages (or whole websites) which are not intended to be viewed inside frames.
|
||||
- Use a covering `<div>` if we want to allow our pages to be shown in iframes, but still stay safe.
|
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<body style="margin:10px;padding:10px">
|
||||
|
||||
<input type="button" onclick="alert('Like pressed on facebook.html!')" value="I LIKE IT !">
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,32 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: -14px;
|
||||
opacity: 0.5;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>Click to get rich now:</div>
|
||||
|
||||
<!-- The url from the victim site -->
|
||||
<iframe src="facebook.html"></iframe>
|
||||
|
||||
<button>Click here!</button>
|
||||
|
||||
<div>...And you're cool (I'm a cool hacker actually)!</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<body style="margin:10px;padding:10px">
|
||||
|
||||
<input type="button" onclick="alert('Like pressed on facebook.html!')" value="I LIKE IT !">
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,32 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: -14px;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>Click to get rich now:</div>
|
||||
|
||||
<!-- The url from the victim site -->
|
||||
<iframe src="facebook.html"></iframe>
|
||||
|
||||
<button>Click here!</button>
|
||||
|
||||
<div>...And you're cool (I'm a cool hacker actually)!</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,41 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
#protector {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 99999999;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="protector">
|
||||
<a href="/" target="_blank">Go to the site</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
if (top.document.domain == document.domain) {
|
||||
protector.remove();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
This text is always visible.
|
||||
|
||||
But if the page was open inside a document from another domain, the div over it would prevent any actions.
|
||||
|
||||
<button onclick="alert(1)">Click wouldn't work in that case</button>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,12 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<iframe src="iframe.html"></iframe>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,18 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>Changes top.location to javascript.info</div>
|
||||
|
||||
<script>
|
||||
top.location = 'https://javascript.info';
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,41 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -20px;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function attack() {
|
||||
|
||||
window.onbeforeunload = function() {
|
||||
window.onbeforeunload = null;
|
||||
return "Want to leave without learning all the secrets (he-he)?";
|
||||
};
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', '<iframe src="iframe.html">');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>After a click on the button the visitor gets a "strange" question about whether they want to leave.</p>
|
||||
|
||||
<p>Probably they would respond "No", and the iframe protection is hacked.</p>
|
||||
|
||||
<button onclick="attack()">Add a "protected" iframe</button>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1 +0,0 @@
|
|||
# Frames and windows
|
|
@ -1,2 +0,0 @@
|
|||
|
||||
# Miscellaneous
|
Loading…
Add table
Add a link
Reference in a new issue