Come funzionano le regole di sicurezza

La sicurezza può essere uno dei pezzi più complessi del puzzle dello sviluppo delle app. Nella maggior parte delle applicazioni, gli sviluppatori devono creare ed eseguire un server che gestisce l'autenticazione (chi è l'utente) e l'autorizzazione (cosa può fare un utente).

Firebase Security Rules rimuove il livello centrale (server) e ti consente di specificare in base al percorso autorizzazioni per i client che si connettono direttamente ai tuoi dati. Utilizza questa guida per Scopri di più su come vengono applicate le regole alle richieste in entrata.

Seleziona un prodotto per scoprire di più sulle relative regole.

Cloud Firestore

Struttura di base

Firebase Security Rules in Cloud Firestore e Cloud Storage utilizzano la seguente struttura e sintassi:

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

È importante comprendere i seguenti concetti chiave durante la creazione delle regole:

  • Richiesta: il metodo o i metodi richiamati nell'istruzione allow. Si tratta di che consenti l'esecuzione. I metodi standard sono: get, list, create, update e delete. Metodi di praticità di read e write abilita l'accesso ampio in lettura e scrittura sul database o sul percorso di archiviazione specificato.
  • Percorso:il database o la posizione di archiviazione, rappresentata come del percorso dell'URI.
  • Regola: l'istruzione allow, che include una condizione che consente una richiesta se restituisce true.

Regole di sicurezza versione 2

A partire da maggio 2019, la versione 2 delle regole di sicurezza Firebase è ora disponibile disponibili. La versione 2 delle regole modifica il comportamento degli annunci ricorrenti caratteri jolly {name=**}. Devi utilizzare la versione 2 se prevedi di Utilizzare le query del gruppo di raccolte. Devi attivare versione 2 impostando rules_version = '2'; come prima riga nella sezione regole:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

Percorsi corrispondenti

Tutte le dichiarazioni di corrispondenza devono puntare a documenti, non a raccolte. Una corrispondenza l'istruzione può puntare a un documento specifico, ad esempio match /cities/SF, o utilizzare caratteri jolly per puntare a qualsiasi documento nel percorso specificato, come in match /cities/{city}.

Nell'esempio precedente, l'istruzione di corrispondenza utilizza la sintassi con caratteri jolly {city}. Ciò significa che la regola si applica a qualsiasi documento della raccolta cities, ad esempio /cities/SF o /cities/NYC. Quando le espressioni allow nell'istruzione di corrispondenza vengono valutato, la variabile city si risolverà nel nome del documento della città, come SF o NYC.

Raccolte secondarie corrispondenti

I dati in Cloud Firestore sono organizzati in raccolte di documenti e ogni documento può estendere la gerarchia tramite sottocollezioni. È importante Comprendere in che modo le regole di sicurezza interagiscono con i dati gerarchici.

Considera la situazione in cui ogni documento nella raccolta cities contiene una landmarks raccolta secondaria. Le regole di sicurezza si applicano solo al percorso corrispondente, pertanto i controlli dell'accesso definiti nella raccolta cities non si applicano al landmarks raccolta secondaria. Scrivi invece regole esplicite per controllare l'accesso alle sottoraccolte:

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      allow read, write: if <condition>;

      // Explicitly define rules for the 'landmarks' subcollection
      match /landmarks/{landmark} {
        allow read, write: if <condition>;
      }
    }
  }
}

Quando si nidificano le istruzioni match, il percorso dell'istruzione match interna è sempre rispetto al percorso dell'istruzione match esterna. Pertanto, i seguenti set di regole sono equivalenti:

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      match /landmarks/{landmark} {
        allow read, write: if <condition>;
      }
    }
  }
}
service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city}/landmarks/{landmark} {
      allow read, write: if <condition>;
    }
  }
}

Caratteri jolly ricorsivi

Se vuoi che le regole vengano applicate a una gerarchia arbitrariamente profonda, utilizza sintassi con caratteri jolly ricorsivi, {name=**}:

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

Quando si utilizza la sintassi con caratteri jolly ricorsivi, la variabile con caratteri jolly conterrà il carattere l'intero segmento di percorso corrispondente, anche se il documento si trova in una zona profondamente nidificata una sottoraccolta. Ad esempio, le regole elencate sopra corrispondono un documento situato in /cities/SF/landmarks/coit_tower e il valore la variabile document è SF/landmarks/coit_tower.

Tieni presente, tuttavia, che il comportamento dei caratteri jolly ricorsivi dipende dalle regole completamente gestita.

Versione 1

Le regole di sicurezza utilizzano la versione 1 per impostazione predefinita. Nella versione 1, i caratteri jolly ricorsivi corrisponde a uno o più elementi del percorso. Non corrispondono a un percorso vuoto, quindi match /cities/{city}/{document=**} corrisponde ai documenti nelle sottoraccolte, ma non nella raccolta cities, mentre match /cities/{document=**} corrisponde a entrambi i documenti nella raccolta cities e nelle sottoraccolte.

I caratteri jolly ricorsivi devono essere inseriti alla fine di un'istruzione di corrispondenza.

Versione 2

Nella versione 2 delle regole di sicurezza, i caratteri jolly ricorsivi corrispondono a zero o più percorsi elementi. match/cities/{city}/{document=**} corrisponde a documenti in qualsiasi nonché i documenti della raccolta cities.

Devi attivare la versione 2 aggiungendo rules_version = '2'; nella parte superiore di le tue regole di sicurezza:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{city}/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

È possibile avere al massimo un carattere jolly ricorsivo per ciascuna istruzione di corrispondenza, ma nella versione 2, puoi inserire questo carattere jolly in qualsiasi punto dell'istruzione di corrispondenza. Ad esempio:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the songs collection group
    match /{path=**}/songs/{song} {
      allow read, write: if <condition>;
    }
  }
}

Se utilizzi le query dei gruppi di raccolte, devi utilizzare versione 2, consulta la sezione Protezione delle gruppo di raccolte di raccolte.

Estratti conto di corrispondenza sovrapposti

È possibile che un documento corrisponda a più dichiarazioni match. Nella nel caso in cui più espressioni allow corrispondano a una richiesta, l'accesso è consentito se una delle condizioni è true:

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the 'cities' collection.
    match /cities/{city} {
      allow read, write: if false;
    }

    // Matches any document in the 'cities' collection or subcollections.
    match /cities/{document=**} {
      allow read, write: if true;
    }
  }
}

Nell'esempio precedente, tutte le letture e le scritture nella raccolta cities saranno consentito perché la seconda regola è sempre true, anche se la prima è sempre false.

Limiti delle regole di sicurezza

Quando lavori con le regole di sicurezza, tieni presente i seguenti limiti:

Limite Dettagli
Numero massimo di chiamate exists(), get() e getAfter() per richiesta
  • 10 per richieste di documenti singoli e di query.
  • 20 per transazioni, operazioni di scrittura in batch e operazioni di lettura di più documenti. A ciascuna operazione si applica anche il limite precedente di 10.

    Ad esempio, immagina di creare una richiesta di scrittura in batch con 3 operazioni di scrittura e che le tue regole di sicurezza utilizzino 2 chiamate di accesso ai documenti per convalidare ogni operazione di scrittura. In questo caso, ogni operazione di scrittura utilizza 2 delle sue 10 chiamate di accesso e la richiesta di scrittura in batch utilizza 6 delle sue 20 chiamate di accesso.

Il superamento di uno dei limiti comporta un errore di autorizzazione negata.

Alcune chiamate di accesso ai documenti possono essere memorizzate nella cache e le chiamate nella cache non vengono considerate ai fini dei limiti.

Profondità massima delle istruzioni match nidificate 10
Lunghezza massima del percorso, in segmenti di percorso, consentita all'interno di un set di istruzioni match nidificate 100
Numero massimo di variabili di acquisizione percorso consentite all'interno di un set di istruzioni match nidificate 20
Profondità massima delle chiamate funzione 20
Numero massimo di argomenti di funzione 7
Numero massimo di associazioni di variabili let per funzione 10
Numero massimo di chiamate di funzione ricorsive o cicliche 0 (non consentite)
Numero massimo di espressioni valutate per richiesta 1000
Dimensione massima di un set di regole I set di regole devono rispettare due limiti di dimensione:
  • un limite di 256 kB per la dimensione dell'origine di testo del set di regole pubblicato dalla console Firebase o dall'interfaccia a riga di comando utilizzando firebase deploy.
  • Un limite di 250 kB per la dimensione del set di regole compilato che risulta quando Firebase elabora l'origine e la rende attiva sul back-end.

Cloud Storage

Struttura di base

Firebase Security Rules in Cloud Firestore e Cloud Storage utilizzano la seguente struttura e sintassi:

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

È importante comprendere i seguenti concetti chiave durante la creazione delle regole:

  • Richiesta: il metodo o i metodi richiamati nell'istruzione allow. Si tratta di che consenti l'esecuzione. I metodi standard sono: get, list, create, update e delete. Metodi di praticità di read e write abilita l'accesso ampio in lettura e scrittura sul database o sul percorso di archiviazione specificato.
  • Percorso:il database o la posizione di archiviazione, rappresentata come del percorso dell'URI.
  • Regola: l'istruzione allow, che include una condizione che consente una richiesta se restituisce true.

Percorsi corrispondenti

Cloud Storage Security Rules match i percorsi file utilizzati per accedere ai file in Cloud Storage. Le regole possono match percorsi esatti o percorsi con caratteri jolly e possono anche essere nidificate. Se nessuna regola di corrispondenza consente un metodo di richiesta, oppure la condizione restituisce false e la richiesta viene rifiutata.

Corrispondenze esatte

// Exact match for "images/profilePhoto.png"
match /images/profilePhoto.png {
  allow write: if <condition>;
}

// Exact match for "images/croppedProfilePhoto.png"
match /images/croppedProfilePhoto.png {
  allow write: if <other_condition>;
}

Corrispondenze nidificate

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/profilePhoto.png"
  match /profilePhoto.png {
    allow write: if <condition>;
  }

  // Exact match for "images/croppedProfilePhoto.png"
  match /croppedProfilePhoto.png {
    allow write: if <other_condition>;
  }
}

Corrispondenze con caratteri jolly

Le regole possono essere utilizzate anche per match un pattern utilizzando caratteri jolly. Un carattere jolly è un variabile con nome che rappresenta una singola stringa come profilePhoto.png o più segmenti del percorso, come images/profilePhoto.png.

Un carattere jolly viene creato aggiungendo parentesi graffe intorno al nome, come {string}. È possibile dichiarare un carattere jolly per più segmenti aggiungendo =** al nome con caratteri jolly, ad esempio {path=**}:

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/*"
  // e.g. images/profilePhoto.png is matched
  match /{imageId} {
    // This rule only matches a single path segment (*)
    // imageId is a string that contains the specific segment matched
    allow read: if <condition>;
  }

  // Exact match for "images/**"
  // e.g. images/users/user:12345/profilePhoto.png is matched
  // images/profilePhoto.png is also matched!
  match /{allImages=**} {
    // This rule matches one or more path segments (**)
    // allImages is a path that contains all segments matched
    allow read: if <other_condition>;
  }
}

Se più regole corrispondono a un file, il risultato è il OR del risultato di tutte e valutazioni di regole. In altre parole, se una regola corrispondente al file restituisce true, il il risultato è true.

Nelle regole precedenti, il file "images/profilePhoto.png" può essere letto se condition o other_condition restituiscono true, mentre il file "images/users/user:12345/profilePhoto.png" è soggetto solo al risultato other_condition.

È possibile fare riferimento a una variabile jolly all'interno dell'autorizzazione del nome o del percorso del file fornito in match:

// Another way to restrict the name of a file
match /images/{imageId} {
  allow read: if imageId == "profilePhoto.png";
}

Cloud Storage Security Rules non vengono applicati a cascata e le regole vengono valutate solo quando il percorso della richiesta corrisponde a un percorso con le regole specificate.

Richiedi valutazione

Caricamenti, download, modifiche ed eliminazioni dei metadati vengono valutati utilizzando Importo di request inviato a Cloud Storage. La variabile request contiene il parametro il percorso del file in cui viene eseguita la richiesta, l'ora in cui ricevuto e il nuovo valore resource se la richiesta è una scrittura. Sono inclusi anche le intestazioni HTTP e lo stato di autenticazione.

L'oggetto request contiene anche l'ID univoco dell'utente e Payload Firebase Authentication nell'oggetto request.auth, che verrà spiegata più dettagliatamente nella sezione Autenticazione sezione dei documenti.

Di seguito è disponibile un elenco completo delle proprietà nell'oggetto request:

Proprietà Tipo Descrizione
auth mappa<stringa, stringa> Quando un utente accede, fornisce uid, l'ID univoco dell'utente e token, una mappa di Firebase Authentication dichiarazioni JWT. In caso contrario, sarà null.
params mappa<stringa, stringa> Mappa contenente i parametri di query della richiesta.
path percorso Un path che rappresenta il percorso della richiesta è stato eseguito.
resource mappa<stringa, stringa> Il nuovo valore della risorsa, presente solo nelle richieste write.
time timestamp Un timestamp che rappresenta l'ora del server in cui viene valutata la richiesta.

Valutazione delle risorse

Durante la valutazione delle regole, è consigliabile esaminare anche i metadati del file in fase di caricamento, download, modifica o eliminazione. Questo consente di creare regole complesse ed efficaci, ad esempio consentire solo i file con tipi di contenuti da caricare o solo i file di dimensioni superiori a una determinata dimensione eliminati.

Firebase Security Rules per Cloud Storage fornisce i metadati dei file in resource contenente le coppie chiave/valore dei metadati mostrati in un Oggetto Cloud Storage. Queste proprietà possono essere ispezionate su read o Richieste di write per garantire l'integrità dei dati.

Per le richieste di write (come caricamenti, aggiornamenti ed eliminazioni di metadati), in aggiunta all'oggetto resource, che contiene i metadati del file attualmente esistente nel percorso di richiesta, puoi anche utilizzare request.resource, che contiene un sottoinsieme dei metadati del file da utilizzare se la scrittura è consentita. Puoi utilizzare questi due valori per garantire l'integrità dei dati o applicare vincoli di applicazione come il tipo o le dimensioni del file.

Di seguito è disponibile un elenco completo delle proprietà nell'oggetto resource:

Proprietà Tipo Descrizione
name stringa Il nome completo dell'oggetto
bucket stringa Il nome del bucket in cui si trova questo oggetto.
generation int La Google Cloud Storage di questo oggetto.
metageneration int La Google Cloud Storage della metagenerazione dell'oggetto.
size int Le dimensioni dell'oggetto in byte.
timeCreated timestamp Un timestamp che rappresenta l'ora in cui è stato creato un oggetto.
updated timestamp Un timestamp che rappresenta l'ora dell'ultimo aggiornamento di un oggetto.
md5Hash stringa Un hash MD5 dell'oggetto.
crc32c stringa Un hash crc32c dell'oggetto.
etag stringa L'etag associato a questo oggetto.
contentDisposition stringa La disposizione dei contenuti associata a questo oggetto.
contentEncoding stringa La codifica dei contenuti associata a questo oggetto.
contentLanguage stringa La lingua dei contenuti associata a questo oggetto.
contentType stringa Il tipo di contenuti associato a questo oggetto.
metadata mappa<stringa, stringa> Coppie chiave/valore di metadati personalizzati aggiuntivi specificati dallo sviluppatore.

request.resource contiene tutti questi valori tranne generation, metageneration, etag, timeCreated e updated.

Limiti delle regole di sicurezza

Quando lavori con le regole di sicurezza, tieni presente i seguenti limiti:

Limite Dettagli
Numero massimo di firestore.exists() e firestore.get() chiamate per richiesta

2 per le richieste di documenti singoli e di query.

Il superamento di questo limite comporta un errore di autorizzazione negata.

Le chiamate di accesso agli stessi documenti possono essere memorizzate nella cache e le chiamate memorizzate nella cache non vengono conteggiate ai fini dei limiti.

Esempio completo

Riassumendo, puoi creare un esempio completo di regole per un'immagine soluzione di archiviazione:

service firebase.storage {
 match /b/{bucket}/o {
   match /images {
     // Cascade read to any image type at any path
     match /{allImages=**} {
       allow read;
     }

     // Allow write files to the path "images/*", subject to the constraints:
     // 1) File is less than 5MB
     // 2) Content type is an image
     // 3) Uploaded content type matches existing content type
     // 4) File name (stored in imageId wildcard variable) is less than 32 characters
     match /{imageId} {
       allow write: if request.resource.size < 5 * 1024 * 1024
                    && request.resource.contentType.matches('image/.*')
                    && request.resource.contentType == resource.contentType
                    && imageId.size() < 32
     }
   }
 }
}

Realtime Database

Struttura di base

In Realtime Database, Firebase Security Rules è costituito da espressioni simili a JavaScript contenute in un oggetto documento JSON.

Utilizzano la seguente sintassi:

{
  "rules": {
    "<<path>>": {
    // Allow the request if the condition for each method is true.
      ".read": <<condition>>,
      ".write": <<condition>>,
      ".validate": <<condition>>
    }
  }
}

La regola include tre elementi di base:

  • Percorso:la posizione del database. Questo rispecchia la struttura JSON del database.
  • Richiesta: sono i metodi utilizzati dalla regola per concedere l'accesso. read e write regole concedono accesso ampio in lettura e scrittura, mentre validate regole agire come verifica secondaria per concedere l'accesso in base alle richieste in arrivo o e i dati di Google Cloud.
  • Condizione:la condizione che consente una richiesta se restituisce true.

Come si applicano le regole ai percorsi

In Realtime Database, i Rules vengono applicati a livello atomico, il che significa che le regole i nodi principali di livello superiore eseguono l'override delle regole nei nodi figlio più granulari a un nodo più profondo non possono concedere l'accesso a un percorso padre. Tu non puoi perfezionare o revocare l'accesso in un percorso più approfondito nella struttura del database se lo hai già concesso per uno dei percorsi principali.

Tieni presente le seguenti regole:

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

Questa struttura di sicurezza consente la lettura di /bar/ da quando /foo/ contiene un elemento secondario baz con valore true. La regola ".read": false in /foo/bar/ non ha perché l'accesso non può essere revocato da un percorso figlio.

Potrebbe non sembrare immediatamente intuitivo, ma si tratta di una parte importante del e consente l'implementazione di privilegi di accesso molto complessi con il minimo sforzo. Questo è particolarmente utile per la sicurezza basata sugli utenti.

Tuttavia, le regole .validate non vengono applicate a cascata. Affinché una scrittura sia consentita, tutte le regole di convalida devono essere soddisfatte a tutti i livelli della gerarchia.

Inoltre, poiché le regole non vengono applicate nuovamente a un percorso principale, le operazioni di lettura o scrittura non vanno a buon fine se non è presente una regola nella posizione richiesta o in una posizione principale che concede l'accesso. Anche se ogni percorso figlio interessato è accessibile, la lettura nella posizione principale non riuscirà del tutto. Considera questa struttura:

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

Se non si comprende che le regole vengono valutate a livello atomico, ad esempio, il recupero del percorso /records/ restituirà rec1 ma non rec2. Il risultato effettivo, tuttavia, è un errore:

JavaScript
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
Nota: questo prodotto Firebase non è disponibile come target di App Clip.
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
Nota: questo prodotto Firebase non è disponibile nel target dell'app clip.
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
})
Java
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

Poiché l'operazione di lettura in /records/ è atomica e non esiste alcuna regola di lettura che concede l'accesso a tutti i dati in /records/, verrà generato un errore PERMISSION_DENIED. Se valutiamo questa regola nel simulatore di sicurezza nella console Firebase, l'operazione di lettura è stata rifiutata:

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

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

L'operazione è stata rifiutata perché nessuna regola di lettura consentiva l'accesso al percorso /records/, ma tieni presente che la regola per /records/ non è mai stata valutata perché non era nel percorso che abbiamo richiesto. Per recuperare rec1, dobbiamo accedervi direttamente:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
Nota: questo prodotto Firebase non è disponibile come target di App Clip.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
Nota: questo prodotto Firebase non è disponibile nel target dell'app clip.
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
Java
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!

Variabile posizione

Realtime Database Rules support a $location per far corrispondere i segmenti del percorso. Utilizza il prefisso $ davanti al percorso per abbinare la regola ai nodi figlio lungo il percorso.

  {
    "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')"
          }
        }
      }
    }
  }

Puoi anche utilizzare $variable in parallelo al percorso costante i nomi degli utenti.

  {
    "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 }
      }
    }
  }