If you are not scared by the
first part , I suggest to continue the conversation about the security mechanisms of Meteor. Starting with the
loginToken issued to the client,
allow / deny rules when the client
modifies the database, touch
trusted and untrusted code ,
server methods ,
use of HTTPS and force-ssl ,
browser-policy (Content Security Policy and X-Frame-Options), and finish with the
built-in data validation mechanism (the check () function and the audit-arguments-check package).
loginToken
After authorization, the client receives a temporary token that authorizes the current user, which is stored in localStorage:
> localStorage.getItem("Meteor.loginToken") "eEg4T3fNPGLns7MfY"
Strictly speaking, it is stored in the
Meteor._locaStorage object, which is a window.localStorage wrapper for browsers that support it.
You can also find out this token through the object Accounts:
Accounts._storedLoginToken()
The same token is stored on the server in the
Meteor.users collection:
> Meteor.user().services.resume { "loginTokens": [ { "token":"DXC3BqekpPy97fmYs", "when":"2014-01-31T10:53:54.347Z" } ] }
Of course, in the browser console this field is available only if it is explicitly published.
Any browser that has a pair of token + user ID is considered authorized. To verify this, you can log in to the browser and get the current loginToken and userId:
localStorage.getItem("Meteor.loginToken"); localStorage.getItem("Meteor.userId");
Then install them in another browser:
localStorage.setItem("Meteor.loginToken", "'+loginToken+'"); localStorage.setItem("Meteor.userId", "'+userId+'");
And in a few moments the browser session will be authorized.
Token lifetime
The token exists until the user logs out, or the timeout specified by the parameter has expired (60 days by default):
Accounts.config({loginExpirationInDays: 60})
')
Restricting a client to change a collection - allow / deny rules
If we try to change the services subdocument, it will not work out of the browser:
> Meteor.users.update({ _id: Meteor.userId() }, {$set: { "services.test": "test" } }) undefined update failed: Access denied
This happens because on the server access to this document is restricted by
allow /
deny rules. Let's see how this mechanism is implemented in the
accounts-base package source code:
Meteor.users.allow({
From the code it can be seen that only the document whose userId matches the current user is allowed to change, and you can make changes only in the sub-document profile. The fetch parameter tells Meteor that it is not necessary to receive the entire modified document for checking the credentials (it can be large); only one _id field is enough for checking. Since the allow rule is declared only for the update operation, the insert and remove operations for the client are prohibited:
> Meteor.users.insert({}) "qs8HbcSDjgbgb3vgS" insert failed: Access denied. No allow validators set on restricted collection for method 'insert'.
With the deny rule, you can disable the operation allowed by allow. That is, if one of the allow rules (more than one rule can be specified) returns true, then this permission can be blocked if one of the deny rules returns true, and the record will be denied in this case, despite allow rules.
Verification of access rights to subdocuments
With the update operation, verification capabilities are somewhat limited. For example, if it is necessary to prohibit writing to any field of a subdocument, for example, doc.field1, but allowing it to another field, for example, doc.field2 of our test collection, this will not work. Let us see which parameters are transferred in this case to the rule by adding on the server the output of the input parameters for the allow and deny rules:
Test.allow({ update: function (userId, document, fields, modifier) { console.log('Test.allow(): userId:', userId, '; document:', document, '; fields:', fields, '; modifier:' , modifier); return true; } }); Test.deny({ update: function (userId, document, fields, modifier) { console.log('Test.deny(): userId:', userId, '; document:', document, '; fields:', fields, '; modifier:' , modifier); return false; } });
And perform the update operation for the doc.field1 field, having previously recognized the _id of one of the documents (make sure that the Test collection has the necessary fields published by setting the variable projection = {} in the code of our example, otherwise the result will not be visible):
> Test.findOne({_id: "FG7FaQqYgB7Rs9RDy"}) Object {_id: "FG7FaQqYgB7Rs9RDy", name: "First", value: 1} > Test.update({_id:"FG7FaQqYgB7Rs9RDy"}, { $set: { "doc.field1": "value1" } } ) undefined > Test.findOne({_id: "FG7FaQqYgB7Rs9RDy"}) Object {_id: "FG7FaQqYgB7Rs9RDy", name: "First", value: 1, doc: Object} > Test.findOne({_id: "FG7FaQqYgB7Rs9RDy"}).doc.field1 "value1"
The server log will display:
I20140131-13:31:27.582(4)? Test.deny(): userId: kL7Fkuk29ci4vz8q4 ; document: { _id: 'FG7FaQqYgB7Rs9RDy', name: 'First', value: 1 } ; fields: [ 'doc' ] ; modifier: { '$set': { 'doc.field1': 'value1' } } I20140131-13:31:27.582(4)? Test.allow(): userId: kL7Fkuk29ci4vz8q4 ; document: { _id: 'FG7FaQqYgB7Rs9RDy', name: 'First', value: 1 } ; fields: [ 'doc' ] ; modifier: { '$set': { 'doc.field1': 'value1' } }
In the fields parameter, the passive fields of the topmost level are transferred, that is, based on it, it is possible to determine access rights to the doc field (and all its subdocuments), but it is impossible to apply different rights to the doc.field1 and doc.field fields based on this array. To do this, you can use the modifier parameter, in which an object containing the MongoDb operation is passed, and, in order not to carry out a full analysis of the operation, allow only some hard format and bar all other options like this:
Test.allow({ update: function (userId, user, fields, modifier) { console.log('Test.allow(): userId:', userId, '; document:', document, '; fields:', fields, '; modifier:' , modifier); var setData = modifier["$set"]; return setData && Object.keys(setData).length===1 && setData["doc.field1"]; } });
Of course,
allow /
deny rules work only if the insecure package is removed from the project. By the way, these handlers can also be used for server-side changes made by the client.
Trusted and untrusted code
So far we have changed the record by its identifier. The fact is that the client cannot perform the update operation, specifying something else in the query selector, for example:
Test.update({ value: 1 }, { $set: { "doc.field1": "value1" } } ) Error: Not permitted. Untrusted code may only update documents by ID. [403]
This is due to the fact that Meteor shares trusted and untrusted code. A trusted code is considered to be executed on the server, including server methods called by the client. Untrusted - code executed on the client in the browser.
The untrusted code is allowed to modify documents only one at a time, with an indication of the _id of the document and checking the rules of allow / deny. Also, the upsert operation is prohibited (inserting a document when it is out). The remove operation can similarly be applied only to a single document, with its _id indicated. For more information, see the
docs.meteor.com/#update and
docs.meteor.com/#remove documentation .
Server methods
As an alternative to direct client access to the database, server methods can be used. Since the code executed on the server is considered trusted, it is possible to place the logic of critical operations on the server, prohibiting changes to the corresponding collections on the client. For example, add on the server:
Meteor.startup(function() { Meteor.methods({ testMethod: function(data) { console.log('testMethod(): data:', data); return 'testMethod finished (data:',data,')'; } }); });
And call from the client, passing the last parameter callback, called at the end of the method:
> Meteor.call('testMethod', 'test data', function(err, result) {console.log(err, result);}) undefined undefined "testMethod finished (data:test data)"
Meteor itself does not include HTTPS support, and it requires an intermediate server that terminates the SSL on which the certificate resides. The built-in
force-ssl package allows you to redirect an HTTP connection to an HTTPS URL, excluding connections from localhost.
When using Nginx in this package is not necessary, since the redirection can be implemented as follows
(example of setting Nginx to localhost as a proxy for Meteor, including the generation of an auto-signed certificate)Generate key and certificate
$ openssl genrsa -des3 -out localhost.key 1024 $ openssl req -new -key localhost.key -out localhost.csr Common Name (eg, YOUR name) :localhost $ openssl x509 -req -days 1024 -in localhost.csr -signkey localhost.key -out localhost.crt
Copy the certificate and key to the / etc / nginx / ssl folder
$ mkdir /etc/nginx/ssl $ cp ./localhost.key /etc/nginx/ssl $ cp ./localhost.crt /etc/nginx/ssl
Nginx configuration
Create the /etc/nginx/sites-available/meteor.conf file (if Nginx is installed “from scratch”, you must delete or reconfigure the default file in the same directory that contains the same ports):
server { listen 80; server_name localhost; # $scheme will get the http protocol # and 301 is best practice for tablet, phone, desktop and seo # return 301 $scheme://example.com$request_uri; # We want to redirect people to the https site when they come to the http site. return 301 https://localhost$request_uri; } server { listen 443; server_name localhost; client_max_body_size 500M; access_log /var/log/nginx/meteorapp.access.log; error_log /var/log/nginx/meteorapp.error.log; location / { proxy_pass http://localhost:3000; proxy_set_header X-Real-IP $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } ssl on; ssl_certificate /etc/nginx/ssl/localhost.crt; ssl_certificate_key /etc/nginx/ssl/localhost.key; ssl_verify_depth 3; }
Create link:
ln -s /etc/nginx/sites-available/meteor.conf /etc/nginx/sites-enabled/meteor.conf
Restart Nginx:
$ sudo service nginx restart
Package browser-policy, Content Security Policy and X-Frame-Options
In fact, two other packages are hidden behind
browser-policy , each of which can be used separately, browser-policy-content and browser-policy-framing. The first of these provides an interface for defining
Content Security Policy rules, with the help of which a white list of sources for loading various types of resources is specified. The second is the
X-Frame-Origin parameter, which allows the page to be displayed inside the frame or iframe tags, depending on the site URI that is trying to do this (at the moment, specifying the source URI in X-Frame-Origin only supports Firefox and IE 8+).
Adding a package includes a default policy, while downloading content is allowed only from the same site as the page itself, XMLHTTPRequest requests and WebSocket connections can be directed to any sites. In addition, functions of type eval () are blocked and the application can be included in the frame and iframe only by the same site from which it was downloaded.
At the same time, the following parameters are added to the header of the server response when the page loads:
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline'; x-frame-options: SAMEORIGIN
And, in our example, user images from external sites (Google and Facebook) will no longer be displayed with the following message in the console:
Refused to load the image 'https://lh6.googleusercontent.com/-aCxpjiDMNcM/AAAAAAAAAAI/AAAAAAAAJMY/9hZytqLLZ6Q/photo.jpg' because it violates the following Content Security Policy directive: "img-src data: 'self'".
In order for images from external sites to start displaying again, you need to add the following lines on the server:
Meteor.startup(function() { BrowserPolicy.content.allowImageOrigin("https://*.googleusercontent.com"); BrowserPolicy.content.allowImageOrigin("http://profile.ak.fbcdn.net"); BrowserPolicy.content.allowImageOrigin("http://graph.facebook.com"); });
In this case, the title will look like this:
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src * 'self'; img-src data: 'self' https:/
In addition to the default limitations, the Meteor documentation recommends disabling inline Javascript on the page by calling BrowserPolicy.content.disallowInlineScripts () on the server side (of course, if inline Javascript is not used).
Meteor has a mechanism for validating data passed to server methods and publish functions. For this purpose, the function
check () , which is passed the checked value and the pattern to be checked. A template can be an explicit type indication, or a
Match object that defines more complex validation rules (see http://docs.meteor.com/#match)
Installing the
audit-argument-checks package blocks the execution of the methods and functions of the publication to which data that has not been validated has been transferred.
If validation is not required, you can call the check function with the following parameter
check(arguments, [Match.Any])
Add a package
$ mrt add audit-argument-checks
Now the attempt to call the server method will return an error:
> Meteor.call('testMethod', 'test data', function(err, result) {console.log(err, result);}) undefined errorClass {error: 500, reason: "Internal server error", details: undefined, message: "Internal server error [500]", errorType:"Meteor.Error"…} undefined
And on the server:
Exception while invoking method 'testMethod' Error: Did not check() all arguments during call to 'testMethod'
After adding validation to the server method, it will again correctly work out:
check(data, String);
Instead of conclusion
Trying to share my work, I didn’t notice how big the material turned out to be, despite the fact that I was able to touch only a very, very small part of Meteor.
I hope this text will help you to get to know Meteor and learn something new about it.