콘솔로 이동

데이터 보안

실시간 데이터베이스 규칙은 데이터베이스에 대한 선언적 구성입니다. 즉, 제품 로직과 별도로 규칙이 정의됩니다. 이 방식은 여러 가지 장점을 갖습니다. 클라이언트가 보안 적용을 담당하지 않고, 구현 상의 버그로 인해 데이터의 보안이 침해되지 않으며, 가장 중요한 장점으로 데이터 보호를 위한 서버 등의 중간 관리 체계가 필요하지 않습니다.

규칙 구조화

실시간 데이터베이스 규칙은 JSON 문서에 포함된 자바스크립트와 유사한 표현식으로 작성됩니다. 규칙 구조는 데이터베이스에 저장한 데이터 구조를 따라야 합니다. 예를 들어 메시지 목록을 다음과 같은 형태의 데이터로 유지한다고 가정해 보겠습니다.

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

규칙도 비슷한 구조를 가져야 합니다. 다음은 이 데이터 구조에 사용할 수 있는 규칙 세트의 예입니다.

{
  "rules": {
    "messages": {
      "$message": {
        // only messages from the last ten minutes can be read
        ".read": "data.child('timestamp').val() > (now - 600000)",

        // new messages must have a string content and a number timestamp
        ".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('content').isString() && newData.child('timestamp').isNumber()"
      }
    }
  }
}

보안 규칙의 유형

보안 적용을 위한 규칙에는 .write, .read.validate의 3가지 유형이 있습니다. 각 유형의 간략한 용도는 다음과 같습니다.

규칙 유형
.read 사용자가 데이터를 읽을 수 있는 조건을 기술합니다.
.write 사용자가 데이터를 쓸 수 있는 조건을 기술합니다.
.validate 값의 올바른 형식, 하위 속성을 갖는지 여부 및 데이터 유형을 정의합니다.

사전 정의된 변수

보안 규칙 정의에서 몇 가지 유용한 사전 정의된 변수에 액세스할 수 있습니다. 아래 예제에서는 이들 중 대부분을 사용합니다. 다음은 이러한 변수에 대한 간단한 요약 및 해당 API 참조에 대한 링크입니다.

사전 정의된 변수
now Linux 기점을 기준으로 하는 현재 시간(밀리초)입니다. SDK의 firebase.database.ServerValue.TIMESTAMP로 생성한 타임스탬프를 검증하는 데 특히 유용합니다.
root 작업 시도 전에 Firebase 데이터베이스에 존재한 루트 경로를 나타내는 RuleDataSnapshot입니다.
newData 작업 시도 후에 존재할 데이터를 나타내는 RuleDataSnapshot입니다. 새로 기록되는 데이터와 기존 데이터를 포함합니다.
data 작업 시도 전에 존재한 데이터를 나타내는 RuleDataSnapshot입니다.
$ 변수 ID 및 동적 하위 키를 나타내는 데 사용되는 와일드카드 경로입니다.
auth 인증된 사용자의 토큰 페이로드를 나타냅니다.

규칙에서 위치에 관계없이 이러한 변수를 사용할 수 있습니다. 예를 들어 아래 보안 규칙은 /foo/ 노드에 기록하는 데이터를 100자 미만의 문자열로 제한합니다.

{
  "rules": {
    "foo": {
      // /foo is readable by the world
      ".read": true,

      // /foo is writable by the world
      ".write": true,

      // data written to /foo must be a string less than 100 characters
      ".validate": "newData.isString() && newData.val().length < 100"
    }
  }
}

기존 데이터와 새 데이터 비교

사전 정의된 data 변수는 쓰기 작업이 실행되기 전의 데이터를 참조하는 데 사용됩니다. 반대로, newData 변수는 쓰기 작업에 성공할 때 존재하게 될 새 데이터를 포함합니다. newData는 기록되는 새 데이터와 기존 데이터를 병합한 결과를 나타냅니다.

예를 들어 아래 규칙은 새 레코드를 만들거나 기존 레코드를 삭제하도록 허용하지만 null이 아닌 기존 데이터를 변경할 수 없도록 금지합니다.

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

다른 경로의 데이터 참조

임의의 데이터를 규칙의 기준으로 사용할 수 있습니다. 사전 정의된 root, datanewData 변수를 통해 쓰기 이벤트가 발생하기 전과 후를 기준으로 모든 경로에 액세스할 수 있습니다.

다음 예제에서는 /allow_writes/ 노드의 값이 true이고, 상위 노드에 readOnly 플래그가 설정되지 않았고, 새로 기록되는 데이터에 foo라는 하위 항목이 없을 때 쓰기 작업을 허용합니다.

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

읽기 및 쓰기 규칙의 하위 전파

.read.write 규칙은 하향식으로 작동하며 상위 규칙이 하위 규칙을 재정의합니다. 특정 경로의 규칙이 읽기 또는 쓰기 권한을 부여하는 경우 모든 하위 노드에도 같은 권한이 부여됩니다. 예를 들어 아래 구조를 살펴보세요.

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          /* ignored, since read was allowed already */
          ".read": false
        }
     }
  }
}

이 보안 구조는 /foo/에 포함된 하위 baz의 값이 true일 때 /bar/를 읽을 수 있도록 허용합니다. 하위 경로에서 권한을 취소할 수 없으므로 /foo/bar/ 아래의 ".read": false 규칙은 효과가 없습니다.

이 방식은 다소 직관적이지 않을 수 있으나 규칙 언어의 강력한 기능으로서 매우 복잡한 액세스 권한을 최소한의 노력만으로 구현할 수 있게 해 줍니다. 이 가이드에서 이후에 설명할 사용자 기반 보안 부분에서 이러한 장점이 드러날 것입니다.

.validate 규칙은 하위로 전파되지 않습니다. 즉, 계층구조의 모든 수준에서 모든 검증 규칙이 충족되어야 쓰기가 허용됩니다.

규칙과 필터의 차이

규칙은 유기적으로 적용됩니다. 즉, 해당 위치나 상위 위치에 권한을 부여하는 규칙이 없으면 읽기 또는 쓰기 작업이 즉시 실패합니다. 영향을 받는 하위 경로에 모두 액세스할 수 있다고 해도 상위 위치의 읽기 작업은 모두 실패합니다. 예를 들어 아래 구조를 살펴보세요.

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

규칙을 유기적으로 판단한다는 점을 이해하지 못한 경우 /records/ 경로를 가져올 때 rec2가 아닌 rec1만 반환된다고 생각할 수 있습니다. 그러나 실제로는 오류가 발생합니다.

자바스크립트
var db = firebase.database();
db.ref("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  // success block is not called
} withCancelBlock:^(NSError * _Nonnull error) {
  // cancel block triggered with PERMISSION_DENIED
}];
Swift
var ref = FIRDatabase.database().reference()
ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
자바
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

/records/의 읽기 작업은 원자적이며 /records/ 아래의 모든 데이터에 대해 권한을 부여하는 읽기 규칙이 없으므로 PERMISSION_DENIED 오류가 발생합니다. Firebase 콘솔의 보안 시뮬레이터에서 이 규칙을 판정하면 읽기 작업이 거부되었음을 확인할 수 있습니다.

Attempt to read /records with auth=Success(null)
    /
    /records

No .read rule allowed the operation.
Read was denied.

작업이 거부된 이유는 /records/ 경로에 대한 액세스를 허용하는 읽기 규칙이 없기 때문입니다. rec1에 대한 규칙은 요청된 경로에 없었으므로 판정되지 않았습니다. rec1을 가져오려면 다음과 같이 직접 액세스해야 합니다.

자바스크립트
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
자바
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records/rec1");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

쿼리 기반 규칙

규칙을 필터로 사용할 수는 없지만, 규칙에서 쿼리 매개변수를 사용하면 데이터 중 일부만 액세스 가능하도록 제한할 수 있습니다. 규칙에서 query. 표현식을 사용하여 쿼리 매개변수에 따라 읽기 또는 쓰기 액세스 권한을 부여하면 됩니다.

예를 들어 다음 쿼리 기반 규칙은 사용자 기반 보안 규칙과 쿼리 기반 규칙을 사용하여 baskets 컬렉션의 데이터 중 활성 사용자가 소유한 장바구니에만 액세스할 수 있도록 제한합니다.

"baskets": {
  ".read": "auth.uid != null &&
            query.orderByChild == 'owner' &&
            query.equalTo == auth.uid" // restrict basket access to owner of basket
}

규칙에 쿼리 매개변수를 포함하는 다음 규칙은 성공합니다.

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

그러나 규칙에 매개변수를 포함하지 않는 쿼리는 PermissionDenied 오류와 함께 실패합니다.

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

쿼리 기반 규칙을 사용하여 클라이언트에서 읽기 작업을 통해 다운로드할 수 있는 데이터의 양을 제한할 수도 있습니다.

예를 들어 다음 규칙은 읽기 액세스를 쿼리 결과 중 우선순위에 따라 정렬한 처음 1,000개로 제한합니다.

messages: {
  ".read": "query.orderByKey &&
            query.limitToFirst <= 1000"
}

// Example queries:

db.ref("messages").on("value", cb)                // Would fail with PermissionDenied

db.ref("messages").limitToFirst(1000)
                  .on("value", cb)                // Would succeed (default order by key)

실시간 데이터베이스 규칙에 다음 query. 표현식을 사용할 수 있습니다.

쿼리 기반 규칙 표현식
표현식 유형 설명
query.orderByKey
query.orderByPriority
query.orderByValue
부울 쿼리가 키, 우선순위 또는 값으로 정렬되면 true입니다. 그렇지 않으면 false입니다.
query.orderByChild 문자열
null
하위 노드를 가리키는 상대 경로를 문자열로 표현합니다. 예를 들면 query.orderByChild == "address/zip"과 같습니다. 쿼리가 하위 노드에 따라 정렬되지 않은 경우 이 값은 null입니다.
query.startAt
query.endAt
query.equalTo
문자열
숫자
부울
null
실행 중인 쿼리의 경계를 검색합니다. 경계가 설정되지 않은 경우 null을 반환합니다.
query.limitToFirst
query.limitToLast
숫자
null
실행 중인 쿼리의 한도를 검색합니다. 한도가 설정되지 않은 경우 null을 반환합니다.

데이터 검증

.validate 규칙을 사용하여 데이터 구조를 적용하고 데이터의 형식과 내용을 검증해야 하며, 검증은 .write 규칙에 따라 권한이 부여된 후에만 실행됩니다. 다음은 1900~2099년 사이의 YYYY-MM-DD 형식 날짜만 허용하는 샘플 .validate 규칙 정의로서 정규 표현식을 통해 확인합니다.

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"

.validate 규칙은 하위로 전파되지 않는 유일한 보안 규칙 유형입니다. 하위 레코드 중 하나라도 검증 규칙이 실패하면 전체 쓰기 작업이 거부됩니다. 또한 데이터가 삭제되는 경우, 즉 기록되는 새 값이 null인 경우에는 검증 정의가 무시됩니다.

이러한 점은 중요하지 않아 보일 수 있지만, 실제로는 강력한 Firebase 실시간 데이터베이스 규칙을 작성하는 데 매우 중요합니다. 예를 들어 다음 규칙을 살펴보세요.

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

이 규칙을 염두에 두고 다음 쓰기 작업의 결과를 살펴보세요.

자바스크립트
var ref = db.ref("/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Objective-C
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref child:@"size"] setValue: @99];
Swift
var ref = FIRDatabase.database().reference().child("widget")

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
자바
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
REST
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

이제 같은 구조에서 .validate 대신 .write 규칙을 사용해 봅니다.

{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

이 규칙에서는 다음 작업이 모두 성공합니다.

자바스크립트
var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Objective-C
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];
Swift
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
자바
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
REST
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

이를 통해 .write 규칙과 .validate 규칙의 차이가 드러납니다. 살펴본 것과 같이 위의 모든 규칙은 .validate로 작성되어야 하지만, 삭제 허용 여부에 좌우될 수 있는 newData.hasChildren() 규칙은 예외일 수 있습니다.

$ 변수를 사용하여 경로 세그먼트 포착

$ 접두사로 포착 변수를 선언하여 읽기 또는 쓰기 경로의 일부를 포착할 수 있습니다. 이 변수는 와일드카드 역할을 하며 규칙 선언 내에서 사용할 키 값을 저장합니다.

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

동적 $ 변수를 상수 경로 이름과 함께 사용할 수도 있습니다. 이 예제에서는 $other 변수를 사용하여 widgettitlecolor 이외의 하위 항목이 없도록 하는 .validate 규칙을 선언합니다. 쓰기 작업에서 하위 항목을 추가로 생성하는 경우 작업이 실패합니다.

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}

익명 채팅 예제

이제 규칙을 하나로 결합하여 보안 익명 채팅 애플리케이션을 만들어 보겠습니다. 다음은 규칙 목록이며, 실제로 작동하는 채팅 애플리케이션은 그 아래에 포함되어 있습니다.

{
  "rules": {
    // default rules are false if not specified
    // setting these to true would make ALL CHILD PATHS readable/writable
    // ".read": false,
    // ".write": false,

    "room_names": {
      // the room names can be enumerated and read
      // they cannot be modified since no write rule
      // explicitly allows this
      ".read": true,

      "$room_id": {
        // this is just for documenting the structure of rooms, since
        // they are read-only and no write rule allows this to be set
        ".validate": "newData.isString()"
      }
    },

    "messages": {
      "$room_id": {
        // the list of messages in a room can be enumerated and each
        // message could also be read individually, the list of messages
        // for a room cannot be written to in bulk
        ".read": true,

        // room we want to write a message to must be valid
        ".validate": "root.child('room_names/'+$room_id).exists()",

        "$message_id": {
          // a new message can be created if it does not exist, but it
          // cannot be modified or deleted
          ".write": "!data.exists() && newData.exists()",
          // the room attribute must be a valid key in room_names/ (the room must exist)
          // the object to write must have a name, message, and timestamp
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",

          // the name must be a string, longer than 0 chars, and less than 20 and cannot contain "admin"
          "name": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')" },

          // the message must be longer than 0 chars and less than 50
          "message": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50" },

          // messages cannot be added in the past or the future
          // clients should use firebase.database.ServerValue.TIMESTAMP
          // to ensure accurate timestamps
          "timestamp": { ".validate": "newData.val() <= now" },

          // no other fields can be included in a message
          "$other": { ".validate": false }
        }
      }
    }
  }
}

다음 단계