跳到正文
DEV Community· Alam Fernandez·· 1 天前AI 評分48

我們對自家的多租戶 Firestore 應用發動 51 次攻擊,發現 3 個真實漏洞

We Attacked Our Own Multi-tenant Firestore App 51 Times. Three Holes Were Real.

AI 導讀

團隊以 51 次探測攻擊自家多租戶 Firestore 應用的安全規則,發現 3 個真實漏洞;最終 51 次探測全部被拒。漏洞涉及僅要求用戶登入的授權規則、用戶透過個人資料自行提升角色,以及舊寫入路徑繞過安全檢查;團隊從數據庫核實攻擊沒有改動目標公司的資料。

正文

Disclosure: this post was drafted with AI assistance and reviewed line by line against our engineering records. Every number in it comes from those records. The app is a real production system we operate; it isn't named, and there are no client stories here — the findings are ours, from our own codebase.

The setup

We run a production multi-tenant SaaS on Firebase. Real customers, real storefront data, one shared Firestore database, with companies separated by security rules rather than by infrastructure.

In October 2026 we were about to open self-serve signup — the change that turns "users we invited" into "anyone with an email address." Before that door opened, we wanted an answer to a question code review had never actually given us:

If a signed-in stranger attacks the rules, what happens?

Not "do the rules look right." We had read them plenty of times. We wanted them attacked — the way they'd be attacked in the wild, by an account with no memberships, no role, and no reason to behave.

The method: 51 probes, verified from the data side

We created disposable test identities — fresh accounts with no relationship to any company in the system. Each one signed in, created its own company through the normal onboarding flow (so it was a legitimate owner somewhere), and then tried to cross the line into companies it didn't belong to:

  • Read another company's member roster
  • Write to another company's settings
  • Create orders inside another company
  • Grant itself membership in another company
  • Promote its own role
  • Reset the owner claim

Every probe was logged pass/fail against the rule path that allowed or denied it. And one discipline mattered more than the rest: a denial was only counted when we verified it from the data side. An error message on the client proves the request failed; it doesn't prove nothing was written. We read the database back after the attacks and confirmed the target company's data was untouched.

Final result of that sweep: 51 of 51 probes denied.

That's the headline. It's also the least interesting part of the story — because before the suite passed, the same process found three real vulnerabilities in our own rules. In a codebase we had reviewed, tested, and believed in.

Hole #1: "Any signed-in user" is not an authorization model

Two surfaces in our rules carried the same grant shape:

allow read, write: if isSignedIn();

One guarded our seed-marker collection — small bookkeeping documents the app writes when it initializes a company's demo data. The other was a legacy flat settings collection left over from before the app went multi-tenant.

Both looked harmless. Seed markers are bookkeeping; the settings collection was empty — the document it would one day hold didn't even exist yet.

Baseline probes, run against the live rules before we changed anything:

  • A disposable account with no memberships anywhere wrote a seed marker. HTTP 200.
  • The same class of account created the legacy settings/finance document with an arbitrary closed-period date. HTTP 200.

That second one deserves a pause. The flat settings collection governed nothing yet — enforcement had long since moved to company-scoped settings. But the admin UI still read its display state from it, and the first time anyone clicked "lock period" in that UI, the flat document would exist — writable by every signed-in account on the platform, including accounts belonging to other companies and dormant test accounts. A dormant, empty, legacy surface was a loaded footgun with a roadmap.

The fix. Seed markers now carry a companyId in the document body, and create/update requires that the named company be one the caller is an active member of. Client-side delete is closed entirely — cleanup is an admin operation now. The legacy flat settings write is fenced to managers of the one company whose UI still uses it, with reads unchanged so nothing broke. And after the deploy, we grepped the full ruleset for the pattern: zero remaining write: if isSignedIn() grants anywhere.

Hole #2: Role self-promotion through user documents

The second hole class is a pattern we now believe is sitting in a lot of production Firebase apps: the user profile document that the client can write freely, where a role field on that document feeds an authorization or owner-repair path.

If the user can write their own profile — which most apps allow, for display names and avatars — and anything downstream trusts a role value stored on that document, the user can mint themselves an admin by editing their own profile. No exploit tooling required; it's one document update with the app's own SDK.

The fix is architectural honesty: roles and membership live in membership documents the client cannot self-grant, and rules never trust a client-writable field for authorization. Our probe suite now includes self-promotion attempts as a permanent test case — a disposable identity promoting its own role is one of the 51, and it stays denied.

Hole #3: The legacy write that defeated a guarded block

The third finding was the subtlest. We had a claims-related guard — a block of rules logic intended to control a sensitive path. It was written correctly. It was also quietly bypassable, because a legacy metadata write elsewhere in the ruleset let a signed-in account modify the state the guard depended on.

Nothing about either rule looked wrong in isolation. The vulnerability only existed in the composition — a modern guard standing on a pre-multi-tenant assumption. This is the hole class we think about most now, because it is invisible to a read-through almost by definition: you only find it by tracing what an attacker can write, end to end, and asking what each write unlocks.

The fix closed the legacy write path and re-verified the guard under attack, not just under review.

After the fixes: attacked again

Fixes asserted are not fixes proven, so the hardened rules went through the same treatment: live baseline probes first (both hole #1 surfaces demonstrated open, against live rules, then cleaned up and verified gone), deploy, then post-deploy live probes — 18 of 18: non-member writes denied, a non-member delete attempt on a real marker denied with the marker verified untouched, a fresh disposable owner's complete onboarding sequence (including all six seed-marker writes for its own company) succeeding, and a legitimate manager's normal writes still working. Probe artifacts were deleted and read back as gone; the legitimate flows were verified unbroken, not assumed.

What we took from it

  1. Rules that are only read are rules that are only hoped. Every one of our three holes survived code review. All three fell to scripted attacks with disposable identities. The review isn't useless — it's just not evidence.
  2. Verify denials from the data side. "The client got an error" and "nothing was written" are different claims. Only one of them is security.
  3. Legacy surfaces are the risk. Two of our three holes were pre-multi-tenant leftovers — an old collection, a metadata path — that nobody had re-scoped when the architecture changed. When your data model evolves, your oldest rules are your newest vulnerabilities.
  4. isSignedIn() is an identity check, not an authorization check. The moment a write grant says "any signed-in user," the question isn't whether it will be abused, it's what it unlocks when it is.

If you run a multi-tenant app on Firestore, the honest test is cheap to describe and uncomfortable to run: create an account that belongs to nobody, hand it your own app, and tell it to misbehave. Then read the database and see what it managed to do.


One resource, if you want to run this yourself: we've turned the checklist version of this methodology — the surfaces to probe, the hole classes to test, the verification discipline — into a free Firestore Security Checklist (publishing shortly on Gumroad; link will be added here when it's live). And if you'd rather have it done to your app than by your app: this exact sweep — rules attacked with a probe suite, findings severity-rated, hardened rules and re-test evidence delivered — is available as a fixed-price audit from our team on Contra. No production credentials required; it runs against the emulator and your data model. Questions: afstrategic@gmail.com.

© Alam Fernandez · AF Strategic Technologies LLC

來源:DEV Community · dev.to