Firebase security rule to prevent customers from circumventing usage tracking - firebase

We are offering a service that people will embed on their web site and we are hoping to use Firebase as our backend. We would like to base our subscription rates on page views or something similar. Right now we are stumped trying to figure out how to prevent customers from caching our client js code and omitting any portions that attempt to increment a page views counter.
What we need to do somehow is create a security rule that atomically prevents someone from reading from one location unless they have incremented the counter at another location. Any ideas on how to do this?
For example, assuming the following schema:
{
"comments" : {
"-JYlV8KQGkUk18-nnyHk" : {
"content" : "This is the first comment."
},
"-JYlV8KWNlFZHLbOphFO" : {
"content" : "This is a reply to the first.",
"replyToCommentId" : "-JYlV8KQGkUk18-nnyHk"
},
"-JYlV8KbT63wL9Sb0QvT" : {
"content" : "This is a reply to the second.",
"replyToCommentId" : "-JYlV8KWNlFZHLbOphFO"
},
"-JYlV8KelTmBr7uRK08y" : {
"content" : "This is another reply to the first.",
"replyToCommentId" : "-JYlV8KQGkUk18-nnyHk"
}
},
oldPageViews: 32498,
pageViews: 32498
}
What would be a way of only allowing read access to the comments if the client first incremented the pageViews field? At first I was thinking about having two fields (something like pageViews and oldPageViews) and starting out by incrementing pageViews, reading the comments, then incrementing oldPageViews to match, and only allowing read on comments if pageViews === oldPageViews + 1. However, unless this could be done atomically, the data could get into a corrupt state if the client started the process but didn't finish it.
Here is a codepen trying to test this idea out.

I would suggest a variation of Kato's rate limiting answer : https://stackoverflow.com/a/24841859/75644
Data:
{
"comments": {
"-JYlV8KQGkUk18-nnyHk": {
"content": "This is the first comment."
},
"-JYlV8KWNlFZHLbOphFO": {
"content": "This is a reply to the first.",
"replyToCommentId": "-JYlV8KQGkUk18-nnyHk"
},
"-JYlV8KbT63wL9Sb0QvT": {
"content": "This is a reply to the second.",
"replyToCommentId": "-JYlV8KWNlFZHLbOphFO"
},
"-JYlV8KelTmBr7uRK08y": {
"content": "This is another reply to the first.",
"replyToCommentId": "-JYlV8KQGkUk18-nnyHk"
},
"timestamp" : 1413555509137
},
"pageViews" : {
"count" : 345030,
"lastTs" : 1413555509137
}
}
Security Rules:
{
"rules": {
"pageViews": {
".validate": "newData.hasChildren(['count','lastTs'])",
"count": {
".validate": "newData.exists() && newData.isNumber() && newData.val() > data.val()"
},
"lastTs": {
// timestamp can't be deleted or I could just recreate it to bypass our throttle
".write": "newData.exists()",
// the new value must be at least 500 milliseconds after the last (no more than one message every five seconds)
// the new value must be before now (it will be since `now` is when it reaches the server unless I try to cheat)
".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val()+500)"
}
},
"comments": {
// The comments can't be read unless the pageViews lastTs value is within 500 milliseconds of now
".read": "root.child('pageViews').child('lastTs').val() > now - 501",
".write": true
}
}
}
NOTE : I haven't tested this so you need to play around with it a bit to see if it works.
Also, based on your sample data, I didn't deal with uid's. You need to make sure you're managing who can read/write here.

Justin's adaptation to the throttling code seems like a great starting point. There are a few annoying loopholes left, like forcing the counter to be updated, getting quantifiable metrics/analytics out of your counter (which requires hooking into a stats tool by some means and will be necessary for accurate billing reports and customer inquiries), and also being able to accurately determine when a visit "ends."
Building from Justin's initial ideas, I think a lot of this overhead can be omitted by simplifying the amount the client is responsible for. Maybe something like:
Only force the user to update a timestamp counter
Employ a node.js script to watch for updates to the counter
Let the node.js script "store" the audit data, preferably by
sending it to analytics tools like keen.io, intercom.io, etc.
Starting from this base, I'd adapt the security rules and structure as follows:
{
"rules": {
"count": {
// updated only from node.js script
// assumes our node worker authenticates with a special uid we created
// http://jsfiddle.net/firebase/XDXu5/embedded/result/
".write": "auth.uid === 'ADMIN_WORKER'",
".validate": "newData.exists() && newData.isNumber() && newData.val() > data.val()"
},
"lastTs": {
// timestamp can't be deleted or I could just recreate it to bypass our throttle
".write": "newData.exists()",
// the new value must be equal to now (i.e. Firebase.ServerValue.TIMESTAMP)
".validate": "newData.isNumber() && newData.val() === now"
},
"comments": {
// The comments can't be read unless the pageViews lastTs value is within 30 seconds
".read": "root.child('pageViews').child('lastTs').val() > now - 30000",
"$comment": {
".write": "???"
}
}
}
}
Now I would write a simple node script to perform the count and administrative tasks:
var Firebase = require('firebase');
var ref = new Firebase(URL);
ref.child('lastTs').on('value', heartbeatReceived);
var lastCheck = null;
function heartbeatReceived(snap) {
if( isNewSession(snap.val()) ) {
incrementCounter();
}
updateStatsEngine(snap);
}
function incrementCounter() {
ref.child('count').transaction(function(currVal) {
return (currVal||0) + 1;
});
}
function isNewSession(timestamp) {
// the criteria here is pretty arbitrary and up to you, maybe
// something like < 30 minutes since last update or the same day?
var res = lastCheck === null || timestamp - lastCheck > 30 * 60 * 1000;
lastCheck = timestamp;
return res;
}
function updateStatsEngine(snap) {
// contact keen.io via their REST API
// tell intercom.io that we have an event
// do whatever is desired to store quantifiable stats
// and track billing info
//
//var client = require('keen.io').configure({
// projectId: "<project_id>",
// writeKey: "<write_key>",
// readKey: "<read_key>",
// masterKey: "<master_key>"
//});
//
//client.addEvent("collection", {/* data */});
}
The downside of this approach is that if my admin script goes down, any events during that time are not logged. However, the wonderful thing about this script is its simplicity.
It's not going to have many bugs. Add monit, upstart, or another tool to make sure it stays up and does not crash. Job done.
It's also highly versatile. I can run it on my laptop or even my Android phone (as an HTML page) in a pinch.

Related

Storing User Data Flattened Security

I'd like to create an app that has a lot of user data. Let's say that each user tracks their own time per task. If I were to store it flattened it would look like this:
{
users: {
USER_ID_1: {
name: 'Mat',
tasks: {
TASK_ID_1: true,
TASK_ID_2: true,
...
}
},
},
tasks: {
TASK_ID_1: {
start: 0,
end: 1
},
TASK_ID_2: {
start: 1,
end: 2
}
}
}
Now I'd like to query and get all the task information for the user. Right now the data is small. From their guides: https://www.firebase.com/docs/web/guide/structuring-data.html it says (near the end) "... Until we get into tens of thousands of records..." and then doesn't explain how to handle that.
So my question is as follows. I know we can't do filtering via security, but can I use security to limit what people have access to and then when searching base it off the user id? My structure would then turn to this:
{
users: {
USER_ID_1: {
name: 'Mat'
}
},
tasks: {
TASK_ID_1: {
user: USER_ID_1,
start: 0,
end: 1
},
TASK_ID_2: {
user: USER_ID_1,
start: 1,
end: 2
},
...
}
}
Then I would set up my security rules to only allow each task to be accessed by the user who created it, and my ref query would look like this:
var ref = new Firebase("https://MY_FIREBASE.firebaseio.com/");
$scope.tasks = $firebaseArray(ref.child('tasks/')
.orderByChild('user')
.startAt('USER_ID_1')
.endAt('USER_ID_1'));
Is that how I should structure it? My query works but I'm unsure if it'll work once I introduce security where one user can't see another users tasks.
You've already read that security rules can not be used to filter data. Not even creative data modeling can change that. :-)
To properly secure access to your tasks you'll need something like:
"tasks": {
"$taskid": {
".read": "auth.uid === data.child(user).val()"
}
}
With this each user can only read their own tasks.
But with these rules, your query won't work. At it's most core your query is reading from tasks here:
ref.child('tasks/')...some-filtering...on(...
And since your user does not have read permission on tasks this read operation fails.
If you'd give the user read permission on tasks the read and query would work, but the user could then also read all tasks that you don't want to give them access to.

Firebase allow write without deletion at parent and child nodes

I've been wrestling with Firebase security for a little while now and am not having much luck with a scenario that I don't think is very unique (but it is also not covered in the documentation).
Imagine I have a tree, test_tree, which is at the root of my Firebase database. From there, I have three children under test_tree named requiredString1, requiredString2, and optionalString1.
I would like for an authenticated user of the Firebase to be able to write to test_tree, and require that both of the requiredString children are included, while allowing optionalString1 to be optional. There is one additional caveat which is throwing me for a loop -- although optionalString1 is optional, it should not be allowed to be deleted.
So, with those requirements in mind, I've come up with the following security rules:
"rules" {
"test_tree": {
//Define overall write rules
".write": "
auth !== null &&
newData.exists() //this is done to ensure that a deletion of this tree cannot occur
",
".validate": "
newData.hasChildren(['requiredString1', 'requiredString2'])
",
//Define rules for each child
"requiredString1":{
".validate": "
newData.isString()
"
},
"requiredString2":{
".validate": "
newData.isString()
"
},
"optionalString1":{
".validate": "
newData.isString()
"
},
//And finally, ensure no other miscellaneous children can be written
"$other": {
".validate": false
}
},
//Also, ensure lockdown on all other root trees
"$other": {
".read": false,
".write": false,
".validate": false
}
}
I've started to put together a test suite to test my rules as I go along, but the optional-yet-not-delete rule is causing problems.
With the above rules, I fail on two tests:
A write to test_tree with following payload is allowed (this needs to fail).
{
requiredString1: "string1",
requiredString2: "string2",
optionalString1: null
}
A write to test_tree/optionalString1 with payload of null is allowed (this needs to fail).
I've tried to get tricky with my validation rules, such as:
"rules": {
...
".validate": "
//Ensure that required values are present
newData.hasChildren(['requiredString1', 'requiredString2']) &&
(
//IF optionalString is included, ensure that it's not null
(
newData.hasChild('optionalString1') &&
newData.hasChild('optionalString1').val() !== null
)
||
//But also allow it to be non-present
!newData.hasChild('optionalString1')
)
"
...
}
But, unfortunately, this results in the same errors as before.
I've tried some other rule structures as well including actually moving the entire rule set into each child location (and removing the .write and .validate rules at the parent test_tree location), but then writes to the parent location (that previously worked) would then fail.
Some help here? Again, I would think that allowing data to be optional, yet still prevent deletion, would be a common need.
EDIT 1:
I've spent some time thinking about my question, and I think the requirement is a little bit of a misnomer. Basically, what I was asking for was that the data be optional if and only if it's not present in the Firebase database. If it is present in the Firebase, it essentially becomes required.
However, once I realized that was the true requirement, it made describing the use case easier. Hopefully, it'll be clear in a moment, but basically the use case is protecting the developer that is interfacing with Firebase from themselves.
Imagine there is a tree of several descriptors for an item, and the tree looks like the following:
items: {
item_ID1: {
name: "Item Name",
descriptorA: "A descriptor",
descriptorB: "Another descriptor"
}
}
Basically, I was thinking that descriptorA and descriptorB would be optional, and the name would be required. In the event that name needed to be changed whereas descriptorA and descriptorB would remain the same, I wanted to protect the developer that is writing the interface to this data from being able to accidentally blast descriptorA and descriptorB by using a .set({item_ID1: {name: "New Name"}}).
I think this actually can be accomplished with the following rules:
"rules": {
"items": {
".write": "
auth !== null &&
newData.exists()
",
".validate": "
newData.hasChild('name') &&
(
!data.hasChild('descriptorA') ||
(data.hasChild('descriptorA') && newData.hasChild('descriptorA'))
) &&
(
!data.hasChild('descriptorB') ||
(data.hasChild('descriptorB') && newData.hasChild('descriptorB'))
)
",
"name": {
".validate": "newData.isString()"
",
"descriptorA": {
".validate": "newData.isString()"
",
"descriptorB": {
".validate": "newData.isString()"
",
"$others": {
".validate": false
}
}
}
The first problem is (as you said) that .validate rules are not executed if there is no new data. So you'll need to detect the condition in a .write rule.
The second problem (as you also said) is that "rules cascade", so if you give allow an operation on a higher level node, you cannot take it away on a lower level. For that reason, you'll need to detect the condition on a higher level in the JSON structure.
So the solution is to use a .write rule, higher in the JSON tree.
"test_tree": {
".write": "newData.exists() && (newData.hasChild('optionalString') || !data.hasChild('optionalString'))",
".validate": "newData.hasChildren(['requiredString'])",
"requiredString": {
".validate": "newData.isString()"
},
"optionalString":{
".validate": "newData.isString() || !data.exists()"
},
"$other": {
".validate": false
}
I've simplified your data structure a bit, to only include a single required and a single optional property.
This is all described in EDIT 1 above.
I've spent some time thinking about my question, and I think the requirement is a little bit of a misnomer. Basically, what I was asking for was that the data be optional if and only if it's not present in the Firebase database. If it is present in the Firebase, it essentially becomes required.
However, once I realized that was the true requirement, it made describing the use case easier. Hopefully, it'll be clear in a moment, but basically the use case is protecting the developer that is interfacing with Firebase from themselves.
Imagine there is a tree of several descriptors for an item, and the tree looks like the following:
items: {
item_ID1: {
name: "Item Name",
descriptorA: "A descriptor",
descriptorB: "Another descriptor"
}
}
Basically, I was thinking that descriptorA and descriptorB would be optional, and the name would be required. In the event that name needed to be changed whereas descriptorA and descriptorB would remain the same, I wanted to protect the developer that is writing the interface to this data from being able to accidentally blast descriptorA and descriptorB by using a .set({item_ID1: {name: "New Name"}}).
I think this actually can be accomplished with the following rules:
"rules": {
"items": {
".write": "
auth !== null &&
newData.exists()
",
".validate": "
newData.hasChild('name') &&
(
!data.hasChild('descriptorA') ||
(data.hasChild('descriptorA') && newData.hasChild('descriptorA'))
) &&
(
!data.hasChild('descriptorB') ||
(data.hasChild('descriptorB') && newData.hasChild('descriptorB'))
)
",
"name": {
".validate": "newData.isString()"
",
"descriptorA": {
".validate": "newData.isString()"
",
"descriptorB": {
".validate": "newData.isString()"
",
"$others": {
".validate": false
}
}
}

Firebase Security API - Complex Data Structure - How to enforce relationships?

For the past few weeks i've been exploring Firebase and its features to build a web app, but I've kind of ran into a wall when it comes to security rules.
I've build a data structure on Firebase but I'm not sure if it follows best practices (if it doesn't, feel free to suggest anything different about it):
{
"groups" : {
<GROUP_KEY>
"name": "",
"rels": {
"users": {
<RELS_USERS_KEY>
"key":"" (USER_KEY)
},
"notes": {
<RELS_NOTES_KEY>
"key":"" (NOTE_KEY)
}
},
"isPrivate": true
},
"users": {
<USER_KEY>
"email": "",
"rels": {
"friends": {
<RELS_FRIENDS_KEY>
"key":"" (USER_KEY)
}
},
},
"notes": {
<NOTE_KEY>
"title": "",
"description": "",
"rels": {
"files": {
<RELS_FILES_KEY>
"key":"" (FILE_KEY)
}
}
},
"files": {
<FILE_KEY>
"mode": "",
"url": ""
}
}
The application flow is as follows:
The user signs up: a key is created on "users";
Is redirected to "Groups" view, where he should be shown only
groups that have his ID in RELS > USERS, or that has
"isPrivate":"false";
As the user creates a Group, a new group is added with his ID in RELS > USERS;
Entering the Group view, he should only see notes that are in RELS > NOTES for that group.
The rest of the logic follows the same principle, and I believe that if I can get through the first hurdle of understanding the Firebase security rules and applying them to this case, I can get through the rest.
I've tried a couple of rules, but I can't seem to get any feedback at all from the web application, debugging this has been a trial-and-error process, and its not really working.
Could someone help me at least understanding the logic behind it ? I've read all of their tutorials but they all seem very shallow with no deeper examples on complex structures.
Thanks for the help.
EDIT
I've added the debug:true flag to the login (thanks #Kato), but I'm still getting no feedback on the rules. With the rules as below, I still enter the "Groups" view, but get no feedback on the console, and the logged-in user sees groups he shouldn't:
{
"rules": {
"groups": {
".read": "data.child('rels').child('users/' + auth.user).exists()",
".write": "data.child('rels').child('users/' + auth.user).exists()"
}
}
}
As for the rules I've tried, they were countless, but this is the most recent one (still no feedback).
Maybe I'm missing something ?
Thanks again.
Rules cascade. That is, if any rule allows read, then you cannot revoke it later in a nested child. In this way, you can write rules like the following:
"$record": {
// I can write the entire record if I own it
".write": "data.child('owner').val() === auth.uid",
"foo": {
// anybody in my friends list can write to foo, but not anything else in $record
".write": "data.parent().child('friends/'+auth.uid).exists()"
},
"bar": {
// this is superfluous as permissions are only "granted" and never "revoked" by a child
".write": false
}
}
Note how, because I am the owner, I can also write to foo and to bar, even though bar has tried to revoke my read privilege.
So in your case above, your rules declaration lists read: true which allows full read access to the entire repo. Change that to false and you'll see better results.

Basic user authentication with records in AngularFire

Having spent literally days trying the different, various recommended ways to do this, I've landed on what I think is the most simple and promising. Also thanks to the kind gents from this SO question: Get the index ID of an item in Firebase AngularFire
Curent setup
Users can log in with email and social networks, so when they create a record, it saves the userId as a sort of foreign key.
Good so far. But I want to create a rule so twitter2934392 cannot read facebook63203497's records.
Off to the security panel
Match the IDs on the backend
Unfortunately, the docs are inconsistent with the method from is firebase user id unique per provider (facebook, twitter, password) which suggest appending the social network to the ID. The docs expect you to create a different rule for each of the login method's ids. Why anyone using >1 login method would want to do that is beyond me.
(From: https://www.firebase.com/docs/security/rule-expressions/auth.html)
So I'll try to match the concatenated auth.provider with auth.id to the record in userId for the respective registry item.
According to the API, this should be as easy as
In my case using $registry instead of $user of course.
{
"rules": {
".read": true,
".write": true,
"registry": {
"$registry": {
".read": "$registry == auth.id"
}
}
}
}
But that won't work, because (see the first image above), AngularFire sets each record under an index value. In the image above, it's 0. Here's where things get complicated.
Also, I can't test anything in the simulator, as I cannot edit {some: 'json'} To even authenticate. The input box rejects any input.
My best guess is the following.
{
"rules": {
".write": true,
"registry": {
"$registry": {
".read": "data.child('userId').val() == (auth.provider + auth.id)"
}
}
}
}
Which both throws authentication errors and simultaneously grants full read access to all users. I'm losing my mind. What am I supposed to do here?
I don't think you want to store user-specific data under a non-user-specific index. Instead of push()ing to your firebase reference, store the user data behind a meaningful key.
e.g.
auth.createUser(email, password, function(error, user) {
if (!error) {
usersRef.child(user.id).set(stuff);
}
});
Now you can actually fetch user data based on who is authenticated.
The custom Auth in the forge's simulator isn't the greatest but if you hit the tab key after selecting the input, it lets you paste or edit the field. At which point you can add {"provider":"facebook","id":"63203497"} or {"provider":"twitter","id":"2934392"} and hopefully get some useful debug out of it.
Assuming your firebase is something like:
{"registry":{
"0":{
"id":"abbacadaba123",
"index":"0",
"name":"New Device",
"userId":"facebook63203497"},
"1":{
"id":"adaba123",
"index":"1",
"name":"Other Device",
"userId":"twitter2934392"}
}
}
This may work for security rules:
{
"rules": {
"registry":{
"$registryId":{
".read":"data.child('userId').val() === (auth.provider + auth.id)",
".write":"(data.child('userId').val() === (auth.provider + auth.id))||(auth != null && !data.exists())",
".validate": "newData.hasChildren(['id', 'index', 'name', 'userId'])",
"id": {
".validate":"newData.isString()"
},
"index": {
".validate":"newData.isNumber()"
},
"name": {
".validate":"newData.isString() && newData.val().length >= 1"
},
"userId": {
".validate":"newData.val() === (auth.provider + auth.id)"
}
}
}
}
}
Your read rule tested as expected. The facebook user read-tests true on registry 0 and false on 1. The twitter user is false on 0 and true on 1.
I did a couple quick tests on the .write and .validate rules and they seem to work.
Hope this helps at least rule out the firebase security rules portion of things, so you can focus on the AngularFire binding part.

How to restrict access to data that are not assigned to a specific user?

Let's take the example of http://up2f.co/15euYdT where one can secure the firebase app by checking that only the creator of a comment can change a comment.
Let's assume that we need to keep in another structure the total number of comments, something like
"stats" :{
"comments":{
"count":2
},
}
We need to protect this part from direct access from registered users.We could do something like
"stats" :{
"$adminid":{
"comments":{
"count":2
},
},
}
where we could only allow an admin to have access there.
To do this we would need to create a persistent connection to Firebase that would listen to changes in the comments table and would trigger an event to update the stats table.
Is this possible? If not how else can we secure data that is not assigned to a specific user?
Since your admin process will use a secret token to log in, security rules will not apply. Thus, you can simply secure client access using:
// not applied to privileged server logging in with token
".write": false,
If, alternately, you wanted clients to increment the amount, you could use the following trick, which only allows them to increment the counter, and only allows them to add a comment if the counter has been updated. (See a working demo http://jsfiddle.net/katowulf/5ESSp/)
{
"rules": {
".read": true,
".write": false,
"incid": {
"counter": {
// this counter is set using a transaction and can only be incremented by 1
".write": "newData.isNumber() && ((!data.exists() && newData.val() === 1) || newData.val() === data.val()+1)"
},
"records": {
"$id": {
// this rule allows adds but no deletes or updates
// the id must inherently be in the format rec# where # is the current value of incid/counter
// thus, to add a record, you first create a transaction to update the counter, and then use that counter here
// the value must be a string less than 1000 characters
".write": "$id >= 'rec'+root.child('incid/counter').val() && !data.exists() && newData.isString() && newData.val().length <= 1000"
}
}
}
}
}

Resources