13 skills · 36 min
Skills
Skill 12 of 13
Designs, authors, refactors, and hardens production-grade Cloud Firestore Security Rules (firestore.rules).
12 minutes · 2,534 words · 16 sections
Install
npx skills add firebase/agent-skills --skill firestore-rules-creationnpx skills add firebase/agent-skills/plugin marketplace add firebase/agent-skillsThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
You are an expert Firebase Security Rules engineer with deep knowledge of Firestore security best practices. Your task is to generate comprehensive, secure Firebase Security rules for the user’s project. To minimize the risk of security incidents and avoid misleading the user about the security of their application, you must be extremely humble about the rules you generate. Always present the rules you’ve written as a prototype that needs review.
After generating the rules, you MUST explicitly communicate to the user exactly like this: “I’ve set up prototype Security Rules to keep the data in Firestore safe. They are designed to be secure for . However, you should review and verify them before broadly sharing your app. If you’d like, I can help you harden these rules.”
Follow this structured workflow strictly:
where(), orderBy(), and
limit() clause. The security rules MUST allow these specific queries.CRITICAL: Follow the following principles every time you modify the security rules file
Generate Firebase Security Rules following these principles:
isValidUser) and calling it in
BOTH create and update rules.This means the first firestore.rules file you generate must never have any “allow read: true” statements.
Structure Requirements:
// ===============================================================
// Assumed Data Model
// ===============================================================
//
// This security rules file assumes the following data structures:
//
// Collection: [name]
// Document ID: [pattern]
// Fields:
// - field1: type (required/optional, constraints) - description
// - field2: type (required/optional, constraints) - description
// [List all fields with types, constraints, and whether immutable]
//
// [Repeat for all collections]
//
// ===============================================================This is one of the most important set of instructions to follow. Failing to follow these rules will result in catastrophic security vulnerabilities.
Here’s a bad example of what NOT to do:
match /users/{userId} {
// BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true
allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin();
// BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true
allow update: if (isOwner(userId) &&
Here’s a good example of what TO do:
match /users/{userId} {
// GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role
allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin());
// GOOD: Does NOT allow users to update their own roles unless they are an admin
PREFER USING READ OVER LIST OR GET list and get can add complexity to
security rules. Prefer using read over them.
Date and Timestamp Validation:
timestamp type for date fields.
Firestore automatically ensures they are logically valid dates.isValidDateString only validates format, not logic (it
would accept Feb 31st).\\\\d) in the rules string. Using a single backslash
(\\d) is a common bug that causes validation to fail.Immutable Fields: Fields like createdAt, authorUID, or any other field
that should not change after creation must be explicitly protected in update
rules. (e.g., request.resource.data.createdAt == resource.data.createdAt).
CRITICAL: When allowing non-owners to update specific fields (like
incrementing a counter), you MUST explicitly verify that all other fields
(e.g., authorName, tags, body) remain unchanged to prevent unauthorized
metadata modification. For sensitive fields, ensure that the logged in user is
also the owner of the document.
Identity Integrity: When storing denormalized user identity (e.g.
authorName, authorPhoto), you MUST validate this data.
request.resource.data.authorName == request.auth.token.name.authorUid and fetch the profile client-side. If you denormalize, you
accept the risk of stale or spoofed data unless you validate it.Enforce Strict Schema (No Extraneous Fields): Documents must not contain any fields other than those explicitly defined in the data model. This prevents users from adding arbitrary data.
Secure rules must enforce the application’s business logic. This includes validating field values against a list of allowed options and controlling how and when fields can change.
If a field should only contain specific values (e.g., a status), validate against a list.
Example:
// A 'task' document's status can only be one of three values
function isValidStatus() {
let validStatuses = ['pending', 'in-progress', 'completed'];
return request.resource.data.status in validStatuses;
}
allow create: if isValidStatus() && ...For update operations, you MUST validate that a field is changing from a
valid previous state to a valid new state. This prevents users from bypassing
workflows (e.g., marking a task as ‘completed’ from ‘archived’).
Example:
// A task can only be marked 'completed' if it was 'in-progress'
function validStatusTransition() {
let previousStatus = resource.data.status;
let newStatus = request.resource.data.status;
return (previousStatus == 'in-progress' && newStatus == 'completed') ||
(previousStatus == 'pending' && newStatus == 'in-progress');
For any field that references another resource (like an image path or a parent document ID), you MUST ensure it is correctly scoped to the user or valid within the context.
Example:
// Ensure image path is within the user's own storage folder
allow create: if isScopedPath(request.resource.data.imageBucket) && ...When allowing users to update a counter (like voteCount or answerCount), you
MUST ensure: 1. Atomic Increments: The field is only changing by exactly
+1 or -1. 2. Isolation: NO OTHER FIELDS are being modified. This is
critical to prevent attackers from hijacking the authorName or content while
“voting”. 3. Action Verification: You MUST prevent users from
artificially inflating counts. When incrementing a counter, verify that the user
has not already performed the action (e.g., by checking for the existence of a
‘like’ document) and is not looping updates. * CRITICAL: Relying solely on
!exists(likeDoc) is insufficient because a malicious user can skip creating
the document and loop the increment. * SOLUTION: Use getAfter() to verify
that the corresponding tracking document will exist after the batch completes.
Example:
function isValidCounterUpdate(docId) {
// Allow update only if 'voteCount' is the ONLY field changing
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) &&
// And the change is exactly +1 or -1
math.abs(request.resource.data.voteCount -
While updating the firestore rules, also ensure that the application still works after firestore rules updates.
text.size() < 1000,
tags.size() < 20). Failure to limit a single string field (like caption
or bio) allows 1MB attacks, which is a CRITICAL vulnerability.isDocOwner()isRecent() for timestamps.isPositive() or similar for numbers.isScopedPath() for storage paths.Structure your rules clearly with comments explaining each rule’s purpose.
Critical step: Systematically attempt to break your own rules using the following attack vectors. You MUST document the outcome of each attempt.
visible == false)?get, create, update, or delete a
document that I do not own or have permissions for?create a valid document and then update it
with a 1MB string or invalid fields? (Tests if validation logic is missing
from update).authorUID or ownerId to another user’s ID?update an existing document to
change its authorUID or ownerId?createdAt or other
immutable timestamp or property on an update?number to a field that
should be a string, or a string to a timestamp?create a valid document
and then update it into an invalid state (e.g., remove a required field,
write a string that’s too long)?bio, url, name) MUST have a .size() check. If any
are missing, it’s a “Resource Exhaustion/DoS” risk.create or update a document while
omitting fields that are marked as required in the data model?isAdmin: true to my user profile document? (Tests reliance
on document data vs. custom claims).create or update a document and add an
arbitrary, undefined field like extraData: 'malicious_code'? (Tests for
strict schema enforcement).Document each attack attempt and whether it succeeded. If ANY attack succeeds:
Once devil’s advocate testing passes, repeat until rules pass validation.
After all phases are complete, create or update the firestore.rules file.
Designs, authors, refactors, and hardens production-grade Cloud Firestore Security Rules (firestore.rules). Use when creating security rules, writing schema/domain validators, preventing update bypasses, enforcing type safety and resource limits, or implementing role-based access control. Don't use for security rules auditing (use firebase-security-rules-auditor), database provisioning, or client SDK queries.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
main, last pushed 23 September 2026.SKILL.md, not by matching a directory convention. One layout observed: skills/*/SKILL.md.h1 and no skipped levels:.claude-plugin/marketplace.json by Firebase, declaring 1 plugin. It is read for editorial metadata only — never as the skill index, which is always the repository tree.// ===============================================================
// Helper Functions
// ===============================================================
//
// Check if the user is authenticated
function isAuthenticated() {
return request.auth != null;
}
//
// Check if user owns the resource (for user-owned documents)
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
//
// Check if user is owner based on document's uid field
function isDocOwner() {
return isAuthenticated() && request.auth.uid == resource.data.uid;
}
//
// Verify UID hasn't been tampered with on create
function uidUnchanged() {
return !('uid' in request.resource.data) ||
request.resource.data.uid == request.auth.uid;
}
//
// Ensure uid field is not modified on update
function uidNotModified() {
return !('uid' in request.resource.data) ||
request.resource.data.uid == resource.data.uid;
}
//
// Validate required fields exist
function hasRequiredFields(fields) {
return request.resource.data.keys().hasAll(fields);
}
//
// Validate string length
function validStringLength(field, minLen, maxLen) {
return request.resource.data[field] is string &&
request.resource.data[field].size() >= minLen &&
request.resource.data[field].size() <= maxLen;
}
//
// Validate URL format (must start with https:// or http://)
function isValidUrl(url) {
return url is string &&
(url.matches("^https://.*") || url.matches("^http://.*"));
}
//
// Validate email format
function isValidEmail(email) {
return email is string &&
email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
}
//
// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS)
// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13).
// Use the 'timestamp' type for documents where logical date validation is required.
function isValidDateString(dateStr) {
return dateStr is string &&
dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$");
}
//
// Validate that a string path is correctly scoped to the user's ID
function isScopedPath(path) {
return path is string && path.matches("^users/" + request.auth.uid + "/.*");
}
//
// Validate that a value is positive
function isPositive(field) {
return request.resource.data[field] is number && request.resource.data[field] > 0;
}
//
// Validate that a list is a list and enforces size limits
function isValidList(list, maxSize) {
return list is list && list.size() <= maxSize;
}
//
// Validate optional string (if present, must be string and within length)
function isValidOptionalString(field, minLen, maxLen) {
return !(field in request.resource.data) ||
(request.resource.data[field] is string &&
request.resource.data[field].size() >= minLen &&
request.resource.data[field].size() <= maxLen);
}
//
// Validate that a map contains only allowed keys
function isValidMap(mapData, allowedKeys) {
return mapData is map && mapData.keys().hasOnly(allowedKeys);
}
//
// Validate that the document contains only the allowed fields
function hasOnlyAllowedFields(fields) {
return request.resource.data.keys().hasOnly(fields);
}
//
// Validate that the document hasn't changed in the fields that are not allowed to be changed
function areImmutableFieldsUnchanged(fields) {
return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields);
}
//
// Validate that a timestamp is recent (within the last 5 minutes)
function isRecent(time) {
return time is timestamp &&
time > request.time - duration.value(5, 'm') &&
time <= request.time;
}
//
// [Add more helper functions as needed for the data validation like the example below]
//
// ===============================================================
//
// Domain Validators (CRITICAL: Use these in both create and update)
//
// function isValidUser(data) {
// // Only allow admin to create admin roles
// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) &&
// data.name is string && data.name.size() > 0 && data.name.size() < 50 &&
// data.email is string && isValidEmail(data.email) &&
// data.age is number && data.age >= 18 &&
// data.role in ['admin', 'user', 'guest'];
// }NEVER allow PII EXPOSURE LEAKS: Never allow PII (Personally Identifiable Information) to be exposed in the data model. This includes email addresses, phone numbers, and any other information that could be used to identify a user. For example, even if a user is logged-in, they should not have access to read another user’s information.
No Blanket User Read Access: You are strictly FORBIDDEN from generating
allow read: if isAuthenticated(); for the users collection if that
collection is defined to contain email addresses or other private data.
CRITICAL: Double-Check Blanket isAuthenticated fields: Ensure that paths
that are protected with only isAuthenticated() do not need any additional
checks based on role or any other condition.
The “Ownership-Only Update” Trap: A common critical vulnerability is
allowing updates based solely on ownership (e.g.,
allow update: if isOwner(resource.data.uid);). This allows the owner to
corrupt the data schema, delete required fields, or inject malicious payloads.
You MUST always combine ownership checks with data validation (e.g.,
allow update: if isOwner(...) && isValidEntity(...);) AND validate that
self-escalation is not possible.
Deep Array Inspection: It is insufficient to check if a field is list.
You MUST validate the contents of the array (e.g., ensuring all elements
are strings of a valid UID length) to prevent data corruption or schema
pollution. For example, a tags array must verify that every item is a string
AND that each string is within a reasonable length (e.g., < 20 chars).
Permission-Field Lockdown: Fields that control access (e.g., editors,
viewers, roles, role, ownerId) MUST be immutable for non-owner
editors. In update rules, use areImmutableFieldsUnchanged() for these
fields unless the request.auth.uid matches the document’s original
owner/creator. This prevents “Permission Escalation” where a collaborator
could grant themselves higher privileges or remove the owner.
status'pending''completed''in-progress'imageBucket or profilePic) to a value that points to another user’s data
or a restricted area? (Tests for regex path scoping).createdAt field to the past or
future to bypass sorting or logic? (Tests for request.time validation).price or
quantity) to a negative number or an extremely large one? (Tests for range
validation).likesCount), can I
increment it without creating the corresponding tracking document (e.g.,
inside likes/{userId})? Can I increment it twice? (Tests for getAfter()
consistency checks).users/123/posts/456) if the parent document (users/123) does not exist?
(Tests for parent existence checks).status == 'published', do the rules allow
list only when resource.data.status == 'published'?)update rules (including owner-only
ones) call the isValidX() function? If an allow update rule only checks
isOwner(), it is a CRITICAL vulnerability./firebase/agent-skills.md, and each skill at its own .md URL.