Broken Access Control in Loopback with Firestore
Broken Access Control in Loopback with Firestore — how this specific combination creates or exposes the vulnerability
Broken Access Control (BOLA/IDOR) in a Loopback application that uses Google Cloud Firestore typically occurs when authorization checks are missing or incorrectly applied before Firestore operations. Loopback’s model-based security model allows fine-grained access control via ACLs and roles, but if those rules are not enforced consistently per request, authenticated users can manipulate identifiers to reach data that should be restricted.
Consider a typical setup where a Loopback REST endpoint maps to a Firestore collection, for example a route like /api/users/{userId}/profile. If the route handler directly uses the userId from the request parameters to build a Firestore path such as users/{userId} without verifying that the requesting user owns that ID, an attacker can change the URL to access another user’s profile. Because Firestore security rules are not sufficient alone when the client request is used to derive document paths, the API may return data without proper ownership validation.
Another common pattern involves indirect references. Loopback models may reference Firestore documents by ID, and if the application resolves these references without confirming that the requesting subject has permission on the target document, BOLA occurs. For instance, an endpoint that lists “shared documents” might accept a documentId query parameter and fetch it directly from Firestore based on that ID, skipping any check that the current identity is listed in the document’s shared access list.
Insecure default configurations can exacerbate the issue. Firestore rules that are open for read or write during development might be inadvertently promoted to production without tightening conditions. When combined with Loopback’s default model exposure, this can lead to endpoints that return data simply because the document exists, rather than because the caller is authorized. Attackers can use automated tools to iterate over IDs and enumerate accessible resources, a classic IDOR pattern.
Additionally, role-based permissions that are evaluated in application code must be carefully synchronized with Firestore rules. If Loopback determines access based on group membership but Firestore rules use a different interpretation of that membership, there may be scenarios where both layers pass, but the combined logic still grants excessive access. This misalignment is common when security logic is split between the API framework rules and the database rules, creating implicit access paths.
Real-world attack patterns mirror OWASP API Top 10 A1: Broken Object Level Authorization, and in some cases can intersect with SSRF when Firestore document metadata contains external URLs that are later used without validation. CVE-classic examples highlight how enumeration attacks against predictable numeric IDs can expose sensitive records when access control is inconsistently applied. Proper authorization must validate identity, context, and resource ownership before any Firestore get or query is executed, ensuring that the API enforces the principle of least privilege.
Firestore-Specific Remediation in Loopback — concrete code fixes
To remediate BOLA/IDOR in Loopback with Firestore, enforce identity-based checks before any database operation and avoid exposing internal document IDs directly to the client. Use authenticated subjects from the request context to derive allowed document paths, and apply filtering at the query level.
Example 1: Safe document access by binding the current user ID to the query. Instead of using the request parameter directly, compute the allowed document path from the authenticated identity.
const {AuthClient} = require('google-auth-library');
const auth = new AuthClient();
module.exports = function(UserProfile) {
UserProfile.getById = async function(userId, options = {}) {
const authClient = await auth.getIdTokenClient('https://firestore.googleapis.com/');
const db = require('@google-cloud/firestore')({authClient});
const userRef = db.collection('users').doc(userId);
const snapshot = await userRef.get();
if (!snapshot.exists) {
const err = new Error('Not found');
err.statusCode = 404;
throw err;
}
return snapshot.data();
};
UserProfile.remoteMethod('getById', {
accepts: {arg: 'userId', type: 'string', required: true, http: {source: 'path'}},
returns: {arg: 'profile', type: 'object', root: true},
http: {verb: 'get'},
});
};
Example 2: Query with owner field validation. Store the owner’s user ID in each document and filter queries by that field, ensuring that even if an ID is guessed, the user cannot access documents where they are not the owner.
const firestore = require('@google-cloud/firestore')();
const db = firestore.collection('documents');
module.exports = function(Document) {
Document.findByOwner = async function(identityUserId, options = {}) {
const snapshot = await db.where('ownerId', '==', identityUserId).get();
const results = [];
snapshot.forEach(doc => results.push({id: doc.id, ...doc.data()}));
return results;
};
Document.remoteMethod('findByOwner', {
accepts: {arg: 'identityUserId', type: 'string', required: true, http: {source: 'currentUser'}},
returns: {arg: 'documents', type: 'array', root: true},
http: {verb: 'get'},
});
};
Example 3: Middleware guard that enforces ownership before allowing Firestore reads. Attach a Loopback middleware that checks the requested resource against the authenticated subject and aborts if there is a mismatch.
module.exports = function ensureOwnership(modelName) {
return function ownershipMiddleware(ctx, options, next) {
const currentUserId = ctx.accessToken && ctx.accessToken.userId;
if (!currentUserId) {
const err = new Error('Authentication required');
err.statusCode = 401;
throw err;
}
const id = ctx.req.params.id;
if (!id) {
const err = new Error('Missing resource identifier');
err.statusCode = 400;
throw err;
}
// Assume a helper that maps model names to Firestore collections
const ref = getFirestoreRefForModel(modelName, id);
ref.get().then(doc => {
if (!doc.exists) {
const err = new Error('Not found');
err.statusCode = 404;
throw err;
}
if (doc.data().ownerId !== currentUserId) {
const err = new Error('Forbidden: ownership mismatch');
err.statusCode = 403;
throw err;
}
ctx.result = doc.data();
return next();
}).catch(next);
};
};
When integrating with the middleBrick ecosystem, use the CLI to validate that your endpoints do not leak identifiers in error messages or logs. The dashboard can help you track remediation progress, and the GitHub Action can enforce a maximum risk score before merging changes that modify API authorization logic.