54 lines
2.7 KiB
JavaScript
54 lines
2.7 KiB
JavaScript
|
|
// D3 negative access (LIVE): mint a NON-MEMBER token → raw Firestore REST → expect 403/PERMISSION_DENIED.
|
||
|
|
const admin = require('firebase-admin')
|
||
|
|
const path = require('path')
|
||
|
|
const SA = process.env.SA_JSON || path.join(__dirname, '..', 'closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json')
|
||
|
|
const PROJECT = 'closer-app-22014'
|
||
|
|
const APIKEY = 'AIzaSyDAD7FnEYzhMsil41SzJ1XMjUNnJWmjie8'
|
||
|
|
const COUPLE = process.env.COUPLE_ID || 'Xal3Kw3gjSdn0niERYKJ'
|
||
|
|
const FAKE_UID = 'qa-nonmember-' + Date.now()
|
||
|
|
admin.initializeApp({ credential: admin.credential.cert(require(SA)) })
|
||
|
|
|
||
|
|
const docBase = `https://firestore.googleapis.com/v1/projects/${PROJECT}/databases/(default)/documents`
|
||
|
|
|
||
|
|
async function getIdToken() {
|
||
|
|
const custom = await admin.auth().createCustomToken(FAKE_UID)
|
||
|
|
const r = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${APIKEY}`, {
|
||
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ token: custom, returnSecureToken: true })
|
||
|
|
})
|
||
|
|
const j = await r.json()
|
||
|
|
if (!j.idToken) throw new Error('no idToken: ' + JSON.stringify(j))
|
||
|
|
return j.idToken
|
||
|
|
}
|
||
|
|
|
||
|
|
async function tryGet(token, p, label) {
|
||
|
|
const r = await fetch(`${docBase}/${p}`, { headers: { Authorization: `Bearer ${token}` } })
|
||
|
|
const ok = r.status === 200
|
||
|
|
const verdict = ok ? '❌❌ LEAK (200)' : `✅ DENIED (${r.status})`
|
||
|
|
console.log(` ${label}: ${verdict} [${p}]`)
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
async function trySelfGrant(token, uid) {
|
||
|
|
const r = await fetch(`${docBase}/users/${uid}/entitlements/premium?updateMask.fieldPaths=active`, {
|
||
|
|
method: 'PATCH', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ fields: { active: { booleanValue: true } } })
|
||
|
|
})
|
||
|
|
console.log(` self-grant premium: ${r.status === 200 ? '❌❌ ALLOWED (200)' : `✅ DENIED (${r.status})`}`)
|
||
|
|
return r.status === 200
|
||
|
|
}
|
||
|
|
|
||
|
|
(async () => {
|
||
|
|
console.log('=== D3 non-member negative access (uid', FAKE_UID, ') ===')
|
||
|
|
const tok = await getIdToken()
|
||
|
|
let leaks = 0
|
||
|
|
if (await tryGet(tok, `couples/${COUPLE}`, 'read couple doc')) leaks++
|
||
|
|
if (await tryGet(tok, `couples/${COUPLE}/conversations/main/messages`, 'list messages')) leaks++
|
||
|
|
if (await tryGet(tok, `couples/${COUPLE}/capsules`, 'list capsules')) leaks++
|
||
|
|
if (await tryGet(tok, `couples/${COUPLE}/desire_sync`, 'list desire_sync')) leaks++
|
||
|
|
if (await trySelfGrant(tok, FAKE_UID)) leaks++
|
||
|
|
// cleanup the throwaway auth user
|
||
|
|
await admin.auth().deleteUser(FAKE_UID).catch(() => {})
|
||
|
|
console.log(leaks === 0 ? '=== D3 PASS: all denied ===' : `=== D3 FAIL: ${leaks} LEAK(S) — P0 ===`)
|
||
|
|
process.exit(leaks === 0 ? 0 : 1)
|
||
|
|
})().catch(e => { console.error('FATAL', e.message); process.exit(2) })
|