Fun with Firebase security rules

While working on a facility parking management application https://parkalot.io i experiences some caveats of firebase security rules related to numbers

Firebase security rules are written with a fairly limited subset of JavaScript-like methods, as well as several global variables: now, auth, root.

Application

This seemed enough to implement some server side validation. Our subject is a simple app that say, will keep information about user’s mood each day. Lets assume that we want to create this kind of structure, where each child of node days represents a particular day were we will keep some data. Day will be represented as a number, which means “day since epoch”. For sake of simplicity let’s forget now about existence of timezones.

Desired data structure

Restrictions

  1. don’t allow the user to fill his mood for a day in the future.
  2. don’t allow the user to complain on Fridays, has to be (mood: “isgreat”)
//code that we use to write to database
var
daySinceEpoch = getCurrentEpochDay();
return
firebase
.child('days')
.child(daySinceEpoch)
.child('mood')
.set('awful');

Let’s write some rules! With Bolt Compiler from firebase team it can be as simple as:

// 86400000 - number of milliseconds in a day
path /days/{$epochDay}/{$mood} {
write() { $epochDay < (now / 86400000) }
read() { true }
}

Execution of firebase-bolt rules.bolt gives us:

{
"rules": {
".read": “false”,
".write": “false”,
"days": {
"$epochDay": {
".read": "true",
".write": "$epochDay < now / 86400000"
}
}
}
}

So now lets run.. and see it fail. It is clearly stated in the firebase documentation that we had no chance from the very beginning:

Broken dreams

Yet $key === newData.val()+’’ gives us some hope. If we can cast a number to string, why not cast $epochDay to number with similar operator + (unary plus). Unfortunately, even though firebase doesn’t complain right away as we paste rules into the console, it fails later on, on every single write operation. Even with rules written as below (in JS would be always evaluated to true), we fail in each write operation. Probably in firebase rules we can’t convert string to number that way.

“$epochDay”: {
 “.read”: “true”,
 “.write”: “1 + ‘1’ < 3” // should be always true.
}

In such situation normally we would probably rethink our data structure. Another approach is to add a sibling node to mood, that duplicates information about day but as a number value. In such case we validate their equality as strings, and validate value of {day: xxxxxx} against security rules (see structure below).

Duplicated info about day

Time for some fun

But this time for fun (and fun only) lets create our own way of encoding numbers, to smuggle them into firebase rules. Just like in kindergarten lets assign values to letters. Let a = 1, b = 10, c = 100 etc, so that 151 = cbbbbba. Something similar to what they used about VI century b.c. in ancient Rome. Or wait, let’s borrow the letters from Romans, as they are way more stylish. Now i= 1, x= 10, c = 100, m= 1000, X = 10000, C = 100000, M = 1000000. Please notice that letters for huge numbers (at least in Romans perception) are capitalized! Note also that we don’t use subtractive notation. Use JS snippet below to convert decimal number to our pseudo roman numerals.

function toFirebaseNumber(number) {
var strNum = "" + number;
var digits = ['i', 'x', 'c', 'm', 'X' , 'C', 'M'];
var result = "";
for (var i = 0; i < strNum.length; i++) {
var digit = +strNum[strNum.length - 1 - i];
for (;digit > 0; digit--) {
result = digits[i] + result;
}
}
return result;
}

Now on the firebase rules side it gets a bit more tricky. We want to count how many times each letter appears in the string and recreate the number again. The only string method in firebase rules, that returns a number is .length(), but we can live with that.

// count occurrences of digit
count(digit, str) { str.length - str.replace(digit,'').length }
// parse our notation back to number
parseRoman(str) { count('i',str)
+ count('x',str) * 10
+ count('c',str) * 100
+ count('m',str) * 1000
+ count('X',str) * 10000
+ count('C',str) * 100000 }
// is a proper roman numerical
isRoman(str) { str.matches(/^C{0,9}X{0,9}m{0,9}c{0,9}x{0,9}i{0,9}+$/)
}
// rules, % 7 != 1 because 1 january 1970 was thursday
path /days/{$epochDay}/{$mood} {
write() { isRoman($epochDay)
&& parseInt($epochDay) < now / 86400000
&& (parseInt($epochDay) % 7 != 1 || $mood === 'great')}
read() { true }
}

So now we are sure that our users won’t enter their moods from the future, and that they feel great on Fridays ;). This experiment also shows us, that firebase is quite performant when it comes to processing complex security rules. I noticed no penalty, and both validated and non-validated writes too around 150 ms. At least as long as we don’t kill it with some really spectacular RegExp. Ave!