Open LeadDreamer opened 3 years ago
@LeadDreamer You state you’ve started a wrapper project. Did you consider just making a patch that gets automatically installed at npm install
and exposes the said feature?
I’ve resorted to this approach once with Firebase clients, and now considering it with Raygun client. If npm dependencies are almost there - ”needed code already exists” - I find this a reasonable way to not be kept back by the often sluggish development of libraries that I cannot directly influence.
It is a long standing feature request and we are experimenting with it recently in https://github.com/firebase/firebase-js-sdk/pull/4973. We still need to go through internal API reviews to be able to release it. I can't guarantee when everything will be ready, but stay tuned!
Beyond the .get()
, .getBytes()
or whatever - have you found ANY scenario where the Storage Security rules are relevant? I've set CORS policies - they only apply, it seems, to GCP authentication not Firebase Authentication.
For example:
https://storage.cloud.google.com/theBucket/Artists/recF5G3FqK958V00P/Tours/recUlZr2Wbtu0mfoN/TourStops/reckAynVOU7YmI2fg/image/image.jpg
...can be accessed by me because I have Access to my google account, but cannot be accessed by others. Note this access is INDEPENDENT of security rules. Note it actually gets a 307 status and redirected to:
https://00f74ba44b36a7a4021e61479a11af1c59e94e4c9f-apidata.googleusercontent.com/download/storage/v1/b/myBucket/o/Artists%2FrecF5G3FqK958V00P%2FTours%2FrecUlZr2Wbtu0mfoN%2FTourStops%2FreckAynVOU7YmI2fg%2Fimage%2Fimage.jpg?jk=AFshE3Xo89eif3oJeHMy3o5j6RgsruTw_XlM9_4YAGayXpLkN8fGcpawlpZc0L_nvHjEQKjhbYNP6RShRv2Tgt6u3i0uUsbfFJTP9e7fwfXNgFQ7_iRjn7DSdqZXR7wFxCHBFZlQV_v94ZZaQeIbjfor4CXi9c1SBsjZZE-4G79nLU_LTUn5S1LEzQ4JF0tSbPuFjQ69Gtz8lvVRcjhBKRSs45uvOP7rVFg-k17gr4R_RxyOJdrvfH4UJc-oC3IKpz8DwlNVVSirgdb6Nk25sAnzsOkQnlq9Y76CPL9IRLWbWm7SPBg1r0WntAHvi0CYBUAwcMF_VAVNocxc5Clc4ItzF7SgMvnmKFF5aRO7X-n24cmJ0yjVMHSm31RunDCd9SwWYpqptfe8pAMEH0AAsrm3pWPTL_ZQ4-f8Jlh0FlrYmjoQU-bD1LF6xlUbFJ0slX8cOpt38Y-fiqDQOXQLQ3-IArW_7BfEmhkz17tjLrDt2dlTmVuqy7JFLIDprO_CgaIaQ_fEO6PuJYHYlS-0K5MgRT1SkYCf3I_bxhyQBjg6u3TIZhEHh6VnrDwPPh3B9cO5HSSR0uOYXi4K0Qulm4SvKO1wG_HOUzTvvmCZBdx1Tqd6HUKaugtL2DSLaeEmcOGJFkX_GuhrS3PVEL6LY54-bG7vP35wZcm37HTj0Ui5-J2T5MWELb993ltWKhbSwCWfK-696cwAPNI_TTOFa6hQF8aJYyZJP1fGxJkNM4sjPgPIBogZfPginpgNPGMp0Q0uwHINoQMGXAPGegup9OI_213F0C_dLzK_YZxVpP10_TQDy3m7f619AwEnjT5_rK59GWviJrgb-cONBnCwh0VRf89B9QCIAYux_Jby2Wk4YlCl_R_RUr6AQUjBw5dGr80SPgb1u47nbOhLBEKh4cISnUrBSf1EBvjcQCxmLt9IILHoArNPSf5Y2MapNEtd8Xpsxl4ETi8UcLlF0t7hAH-OrhxhXFCWZJzbB8FUzWxBqQv_g37k8pg9Fp3-9F3b0e2qW20v530zfBTHt7D2sWr14YGamIc8EYiTgK96yA0p38nKHf0b6-Wla6eeowdqE-_xgLqF4Urx8QMdXC6iEOGDpKwLffYJ&isca=1
On the other hand:
https://firebasestorage.googleapis.com/v0/b/myBucket/o/Artists%2Frecq5cURMyJsRKFbO%2FTours%2FrecoVCnOyTvNom0fZ%2FTourStops%2FrecH2Ec5SIatBdGw0%2Fimage%2Fimage.jpg?alt=media&token=whatever-the-token-is
has access by ANYBODY (status 200) whether authenticated or not, while
https://firebasestorage.googleapis.com/v0/b/myBucket/o/Artists%2Frecq5cURMyJsRKFbO%2FTours%2FrecoVCnOyTvNom0fZ%2FTourStops%2FrecH2Ec5SIatBdGw0%2Fimage%2Fimage.jpg?alt=media
cannot be accessed (403 error) by anybody, authenticated or not.
So there is currently literally only the two scenarios:
getDownloadURL()
, which BYPASSES Security RulesI intended on hacking some code to axios or fetch a file from storage into my browser javascript, just to see if it can be done (I doubt it)
With much experimentation, here is what I've learned so far for images stored in Firebase Storage (private information obscured):
=> With a token on the URL, security is bypassed altogether.
=> WITHOUT a token, it is possible to set rules to allow access at various levels of a hierarchy - but ONLY simple on/off, not conditional on request.auth
for example:
WORKS
service firebase.storage {
match /b/{bucket}/o {
match /Artists/{artistID}/{files=**} {
allow read: if true;
}
}
fetched from URL
https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg
the above ALLOWS for images to be fetched IF they are stored under /Artists
, BUT all /
after /o/
MUST be %2F
encoded.
Also, rules can be written to ONLY permit access to LOWER levels in the hierarchy, for example:
service firebase.storage {
match /b/{bucket}/o {
match /Artists/{artistID}/Tours/{tourID}/{files=**} {
allow read: if true;
}
}
fetched from URL
https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2FTours%2FtourID%2Fall%2fthe%2Frest%2Fimage.jpg
will succeed, but
https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fimage.jpg
will properly fail
Note that these work regardless of whether accessed in an authenticated page or not.
WHAT DOES NOT WORK
service firebase.storage {
match /b/{bucket}/o {
match /Artists/{artistID}/{files=**} {
allow read: if request.auth != null;
}
}
fetched from URL
https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg
WILL NOT SUCCEED, whether fetched in an authenticated session (using Auth, Firestore and Cloud Functions) or not.
REFERENCE RESULTS: WORKING EXAMPLE
service firebase.storage {
match /b/{bucket}/o {
match /Artists/{artistID}/{files=**} {
allow read: if true;
}
}
fetched from URL
https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg
HEADERS:
GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg?alt=media
Request Method: GET
Status Code: 200
Remote Address: 74.125.136.95:443
Referrer Policy: strict-origin-when-cross-origin
RESPONSE:
accept-ranges: bytes
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-disposition: inline; filename*=utf-8''image.jpg
content-length: 51471
content-type: image/jpeg
date: Sun, 22 Aug 2021 23:40:23 GMT
etag: "b5a087cf582c6493db5b9ff12f061dfd"
expires: Sun, 22 Aug 2021 23:40:23 GMT
last-modified: Mon, 19 Jul 2021 23:10:01 GMT
server: UploadServer
vary: Origin
x-goog-generation: 1626736201167790
x-goog-hash: crc32c=myFUFA==
x-goog-hash: md5=taCHz1gsZJPbW5/xLwYd/Q==
x-goog-meta-firebasestoragedownloadtokens: 4e17fd71-0847-4f5c-bdd3-e9edc5bc63c1
x-goog-metageneration: 1
x-goog-storage-class: STANDARD
x-goog-stored-content-encoding: identity
x-goog-stored-content-length: 51471
x-guploader-uploadid: ADPycduDIHZeant9PO_UVNzlkfjyIC4qwkmYw7QllKwThxfgpnRhtQazTe_E-BHO47SbauOnlPMREnCe3tup9CzIQUGosUDh-w
REQUEST:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg?alt=media
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://obscured.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
// Active client experiment variation IDs.
repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
QUERY STRING PARAMETERS:
alt: media
FAILING EXAMPLE
service firebase.storage {
match /b/{bucket}/o {
match /Artists/{artistID}/{files=**} {
allow read: if request.auth != null;
}
}
fetched from URL
https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg
HEADERS
GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/mybucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg?alt=media?alt=media
Request Method: GET
Status Code: 403
Remote Address: 64.233.177.95:443
Referrer Policy: strict-origin-when-cross-origin
RESPONSE:
access-control-allow-origin: *
access-control-expose-headers: Content-Range, X-Firebase-Storage-XSRF
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Sun, 22 Aug 2021 23:48:30 GMT
expires: Sun, 22 Aug 2021 23:48:30 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdt8g0ZwZSk-Mjhi80cqJuG4Mn5HHqVT7YC_q6VNjik77yZFRbOWKRFpWB15hBj2Cp4WdOEHYdoHeNvE8UtTQL4
REQUEST:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/myBucket.appspot.com/o/Artists%2Fall%2fthe%2Frest%2fimage.jpg?alt=media?alt=media
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://obscured.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
// Active client experiment variation IDs.
repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
QUERY STRING PARAMETERS:
alt: media
FURTHER REFERENCE, SUCCESSFUL FIRESTORE CALL:
HEADERS
GENERAL:
Request URL: https://firestore.googleapis.com/google.firestore.v1.Firestore/Listen/channel?database=projects%2FmyProject%2Fdatabases%2F(default)&gsessionid=ggRbj_GXcUqsSgaiOVDyViVkuQIrjNweShFZV0Djnsc&VER=8&RID=rpc&SID=ECzssQEhAI8zxYsHShx8ug&CI=0&AID=237&TYPE=xmlhttp&zx=sibt8dlrl1de&t=1
Request Method: GET
Status Code: 200
Remote Address: 142.251.32.202:443
Referrer Policy: strict-origin-when-cross-origin
RESPONSE:
access-control-allow-credentials: true
access-control-allow-origin: https:/obscured.netlify.app
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-encoding: gzip
content-type: text/plain; charset=utf-8
date: Sun, 22 Aug 2021 23:49:20 GMT
server: ESF
vary: origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 0
:authority: firestore.googleapis.com
:method: GET
:path: /google.firestore.v1.Firestore/Listen/channel?database=projects%2FmyProject%2Fdatabases%2F(default)&gsessionid=ggRbj_GXcUqsSgaiOVDyViVkuQIrjNweShFZV0Djnsc&VER=8&RID=rpc&SID=ECzssQEhAI8zxYsHShx8ug&CI=0&AID=237&TYPE=xmlhttp&zx=sibt8dlrl1de&t=1
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
origin: https://obscured.netlify.app
pragma: no-cache
referer: https://obscured.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
// Active client experiment variation IDs.
repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
QUERY STRING PARAMETERS:
database: projects/myProject/databases/(default)
gsessionid: ggRbj_GXcUqsSgaiOVDyViVkuQIrjNweShFZV0Djnsc
VER: 8
RID: rpc
SID: ECzssQEhAI8zxYsHShx8ug
CI: 0
AID: 237
TYPE: xmlhttp
zx: sibt8dlrl1de
t: 1
More interesting data points: In my code I "dynamically" generate a filename:
export const setDefaultImage = () => {
(async () => {
const imageURL = await getDefaultImageURL(
`${COMPANY_NAME.toLowerCase()}_image`
);
setImage(imageURL);
})();
return null;
};
...which calls
/**
* ----------------------------------------------------------------------
* @function
* @static
* @param {!string} key name/key of default image file
* @returns {string}
*/
export const getDefaultImageURL = (key) => {
let filename = key + ".jpg";
return Promise.resolve(
FirebaseStorage.ref("/defaultImages").child(filename).getDownloadURL()
);
};
which sends TWO requests to the backend. FIRST:
https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
which returns
{name: "defaultImages/gigstartr_image.jpg", bucket: "tickets-unlimited-fast.appspot.com",…}
bucket: "tickets-unlimited-fast.appspot.com"
contentDisposition: "inline; filename*=utf-8''gigstartr_image.jpg"
contentEncoding: "identity"
contentType: "image/jpeg"
crc32c: "Q1tIxQ=="
downloadTokens: "12af9fae-afe8-4a12-a124-008014737e7b"
etag: "CMfT3rPdjuwCEAE="
generation: "1601395039381959"
md5Hash: "ktoTPHXMgfe6m1rIkRTB1Q=="
metageneration: "1"
name: "defaultImages/gigstartr_image.jpg"
size: "63487"
storageClass: "STANDARD"
timeCreated: "2020-09-29T15:57:19.381Z"
OBVIOUSLY, .getDownloadURL()
uses this to generate the tokenized URL, also adding ?alt=media&token=12af9fae-afe8-4a12-a124-008014737e7b
to get the media instead of the meta-data.
a SUBSEQUENT call that includes ?alt=media
returns the media content.
WHY IS THIS INTERESTING?
BECAUSE the above call FOLLOWS THE SECURITY RULES. If the rule is set to:
service firebase.storage {
match /b/{bucket}/o {
match /defaultImages/{image} {
allow read: if true;
}
}
a fetch OUTSIDE OF AUTHENTICATED CONTEXT of:
https://firebasestorage.googleapis.com/v0/b/myBucket.appspot.com/o/defaultImages%2Fthat_image.jpg
returns THE METADATA If the rule is set to:
service firebase.storage {
match /b/{bucket}/o {
match /defaultImages/{image} {
allow read: if request.auth.uid != null;
}
}
a fetch OUTSIDE OF AUTHENTICATED CONTEXT of:
https://firebasestorage.googleapis.com/v0/b/myBucket.appspot.com/o/defaultImages%2Fthat_image.jpg
FAILS WITH 403 status - AS IT SHOULD
The INTERESTING part is that WITHIN the authenticated context, the above DOES return the metadata (200 status) - as it should!!!
BUT BUT BUT, we then see
https://firebasestorage.googleapis.com/v0/b/myBucket.appspot.com/o/defaultImages%2Fthat_image.jpg?alt=media
FAILS with a 403 status - THAT fetch requires a token. Security rules SHOULD HAVE ALLOWED THAT FETCH, The only difference is the parameter ?alt=media
So continuing successful vs unsuccessful fetches, both in authentication context:
SUCCESSFUL (SDK call FirebaseStorage.ref("/defaultImages").child(filename).getDownloadURL() ) [note authorization header]
GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
Request Method: GET
Status Code: 200
Remote Address: 142.250.105.95:443
Referrer Policy: strict-origin-when-cross-origin
RESPONSE HEADERS:
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Length, Content-Range, Date, Expires, Server, Transfer-Encoding, X-Firebase-Storage-XSRF, X-GUploader-UploadID, X-Google-Trace
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 586
content-type: application/json; charset=UTF-8
date: Mon, 23 Aug 2021 17:20:29 GMT
expires: Mon, 23 Aug 2021 17:20:29 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycduQtbHJBpytEr5OWYc5NriqklzRZvISMyBf96O6Ww3EBpoQ9Fl3ocRItsNeGbHVY_wKh_lKIbQN5s4Ydb7CNnE
REQUEST HEADERS:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
authorization: Firebase eyJhbGciOiJSUzI1NiIsImtpZCI6IjkwMDk1YmM2ZGM2ZDY3NzkxZDdkYTFlZWIxYTU1OWEzZDViMmM0ODYiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS90aWNrZXRzLXVubGltaXRlZC1mYXN0IiwiYXVkIjoidGlja2V0cy11bmxpbWl0ZWQtZmFzdCIsImF1dGhfdGltZSI6MTYyOTczOTIxMCwidXNlcl9pZCI6Ijd2TE9mdmViM2tnSU5ZWXFjcktxaVFWZVhrNDIiLCJzdWIiOiI3dkxPZnZlYjNrZ0lOWVlxY3JLcWlRVmVYazQyIiwiaWF0IjoxNjI5NzM5MjEyLCJleHAiOjE2Mjk3NDI4MTIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnt9LCJzaWduX2luX3Byb3ZpZGVyIjoiYW5vbnltb3VzIn19.vm8HGD3KdBSx6Fn4_tNSNby0YVf59_t_Ynzc9BCx2zc86-pcnAWCUZrfKyL3jb-csya0TlYFxJdDHTtAzi6V4W4li9H7-kOR3YDRmMdoPSTix9BTeQZo2IPa0M0v2kMYQDGNOL3WrMaeVmP7PhS4Bu9hwRzG2tL3CJ7vSBUhNdCQN5r0NYFnGRBzpXxwxyIPRH1inq4LTm9iiVjCv5itusdxVj3pV9MWNG0rhzNYFVPpLFWUvRGtAmMdRwIi-a6cthiwhMtqoyVWr34TJWILvnL0Pasca_aOIVcTfqU_NPTDpEfjT5pqT9XtRmpBvnVrycPNl6XIgx8HqFH1RIIi7w
cache-control: no-cache
origin: https://friendly-rosalind-62a81a.netlify.app
pragma: no-cache
referer: https://friendly-rosalind-62a81a.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
// Active client experiment variation IDs.
repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
x-firebase-storage-version: webjs/8.9.1
UNSUCCESSFUL (HTML embedded image) [note NO authorization header]
<div style="position: absolute; top: 2%; left: 2%; width: 96%; height: 96%; border-radius: 50%; background-image: url("https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg"); background-position: center center; background-size: 100% 100%;"></div>
with headers:
GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
Request Method: GET
Status Code: 403
Remote Address: 74.125.136.95:443
Referrer Policy: strict-origin-when-cross-origin
RESPONSE HEADERS:
access-control-allow-origin: *
access-control-expose-headers: Content-Range, X-Firebase-Storage-XSRF
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Mon, 23 Aug 2021 17:27:09 GMT
expires: Mon, 23 Aug 2021 17:27:09 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdsNP-4WGZQ7PAJ_tGb_qwzMQMblH01dF7_UK6GK_Spd-ZHxjOdU-76BOw5KD4MVfaa8wowzy2NDw0MkQ7aIFu4
REQUEST HEADERS:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/tickets-unlimited-fast.appspot.com/o/defaultImages%2Fgigstartr_image.jpg
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://friendly-rosalind-62a81a.netlify.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
// Active client experiment variation IDs.
repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
Researching a canvas approach that might help, based on: https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
crossorigin="user-credentials"
with attr crossorigin="user-credentials"
in an authenticated context.
Not only Failed; never got a proper status response
<img src="https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media" crossorigin="use-credentials" alt="good try" style="position: absolute; top: 2%; left: 2%; width: 96%; height: 96%; border-radius: 50%;">
GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media
Referrer Policy: strict-origin-when-cross-origin
RESPONSE HEADERS:
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Length, Content-Range, Date, Expires, Server, Transfer-Encoding, X-Firebase-Storage-XSRF, X-GUploader-UploadID, X-Google-Trace
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Mon, 23 Aug 2021 18:56:04 GMT
expires: Mon, 23 Aug 2021 18:56:04 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdv85GVcVMiK3QQxm1Y8l-NrPv-8QFQhLYoGc2S04Ze_L1HOl-FW6XYAqj3zjzXsLXFXKTS0RH-PW3SfKX7i5dK2vbAcXA
REQUEST HEADERS:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
origin: https://xmy18.csb.app
pragma: no-cache
referer: https://xmy18.csb.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
// Active client experiment variation IDs.
repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
QUERY STRING PARAMETERS
alt: media
crossorigin="anonymous"
When using the following HTML
<img src="https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media" crossorigin="anonymous" alt="good try" style="position: absolute; top: 2%; left: 2%; width: 96%; height: 96%; border-radius: 50%;">
...we receive a 403
status.
background-image-crossorigin: "use-credentials"
with the following HTML
<div style="position: absolute; top: 2%; left: 2%; width: 96%; height: 96%; border-radius: 50%; background-image: url("https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media"); background-position: center center; background-size: 100% 100%;">
we get a 403
status, with headers:
GENERAL:
Request URL: https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media
Request Method: GET
Status Code: 403
Remote Address: 64.233.177.95:443
Referrer Policy: strict-origin-when-cross-origin
RESPONSE HEADERS:
access-control-allow-origin: *
access-control-expose-headers: Content-Range, X-Firebase-Storage-XSRF
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Mon, 23 Aug 2021 19:24:04 GMT
expires: Mon, 23 Aug 2021 19:24:04 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdvqVEiUCshuIZf0PxORQ3DPdpIFyikikocK-LO-KN1idEqayzB6gZiS6F-G-LHpFKv0WAj9p9veEfO9GgXlWG0
REQUEST HEADERS:
:authority: firebasestorage.googleapis.com
:method: GET
:path: /v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media
:scheme: https
accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
accept-encoding: gzip, deflate, br
accept-language: en-US,en;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://xmy18.csb.app/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
sec-fetch-dest: image
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
x-client-data: CLO1yQEIlbbJAQiitskBCMG2yQEIqZ3KAQiMnssBCKCgywEI7/LLAQjN9ssBCLP4ywEInvnLAQj1+csB
Decoded:
message ClientVariations {
// Active client experiment variation IDs.
repeated int32 variation_id = [3300019, 3300117, 3300130, 3300161, 3313321, 3329804, 3330080, 3340655, 3341133, 3341363, 3341470, 3341557];
}
QUERY STRING PARAMETERS
alt: media
Adding crossOrigin:"use-credentials"
to an <img>
should work. Also background-image-crossOrigin: "use-credentials"
. They don't. They get:
Access to image at 'https://firebasestorage.googleapis.com/v0/b/tickets-unlimited-fast.appspot.com/o/Artists%2Frecq5cURMyJsRKFbO%2Fimage%2Fimage.jpg?alt=media' from origin 'https://xmy18.csb.app' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
The reason given isn't really correct, though...
Look at these response headers:
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Length, Content-Range, Date, Expires, Server, Transfer-Encoding, X-Firebase-Storage-XSRF, X-GUploader-UploadID, X-Google-Trace
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: private, max-age=0
content-length: 106
content-type: application/json; charset=UTF-8
date: Tue, 24 Aug 2021 16:02:44 GMT
expires: Tue, 24 Aug 2021 16:02:44 GMT
server: UploadServer
x-content-type-options: nosniff
x-guploader-uploadid: ADPycdsf1M1uKmEWTQbwpTAzPwKIFDn8nO_j72BsmUNhqPmLYtnKNjRGw9fX5a3HaD2_9ZvJcmkMB8xPOfESJPtOBGaqwoGJmw
access-control-allow-credentials is NOT returned by Google's servers. Why isn't it returned by Google's Servers? I don't know.
One of my routines programatically generates a URL, and successfully fetches an image, using the below:
export const setDefaultImage = () => {
(async () => {
const imageURL = await getDefaultImageURL(
`${COMPANY_NAME.toLowerCase()}_image`
);
setImage(imageURL);
})();
return null;
};
export const getDefaultImageURL = (key) => {
let filename = key + ".jpg";
return Promise.resolve(
FirebaseStorage.ref("/defaultImages").child(filename).getDownloadURL()
);
};
What I have observed in the console "network" tab is a GET request for the URL is made, which does NOT return the jpg
- it returns a JSON object with the file metadata. It turns out one of the "secrets" of Firebase Storage is that a request for an object (when allowed) WITH NO PARAMETERS does NOT return the file - it returns the METADATA. Happy undocumented feature. Also noted on that same transaction is a header added to the fetch:
authorization: Firebase eyJhbGciOiJSUzI1NiIsImtpZCI6IjkwMDk1YmM2ZGM2ZDY3NzkxZDdkYTFlZWIxYTU1OWEzZDViMmM0ODYiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiVHJhY3kgSGFsbCIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUF1RTdtQkFtZ2ZwMnBvMERlbUltREtYZ3JoNmJoZlBHOFp3ei0tOEI5ek5uNlUiLCJhcnRpc3QiOnsiSWQiOiJyZWNxNWNVUk15SnNSS0ZiTyIsInJvbGUiOiJtYW5hZ2VyIiwidHlwZSI6IkFydGlzdHMifSwidmVudWUiOm51bGwsImFkbWluIjpmYWxzZSwiYWRtaW5TdGF0dXMiOnRydWUsImJvcmsiOmZhbHNlLCJzdXBlclVzZXIiOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS90aWNrZXRzLXVubGltaXRlZC1mYXN0IiwiYXVkIjoidGlja2V0cy11bmxpbWl0ZWQtZmFzdCIsImF1dGhfdGltZSI6MTYyOTc0MzM0NSwidXNlcl9pZCI6Ill2UEJuVHlsQ3lNT2wwT2FNbWxaZnFkZmlDQzIiLCJzdWIiOiJZdlBCblR5bEN5TU9sME9hTW1sWmZxZGZpQ0MyIiwiaWF0IjoxNjI5ODIwMTY5LCJleHAiOjE2Mjk4MjM3NjksImVtYWlsIjoibGVhZGRyZWFtZXJAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZ29vZ2xlLmNvbSI6WyIxMDQ3MTc2MzMyODcyMDU4Njg4NDYiXSwiZW1haWwiOlsibGVhZGRyZWFtZXJAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoiZ29vZ2xlLmNvbSJ9fQ.lTuvvILwf8jNDU1MZ5pkF0YbZUP7OVaXUaOKHHWHGa7RDgrlWUxP0a211AF8fsOKWUhK_Dt5hk3fnS8cmJx3nQgx8tAazgnCCiP_0FeBJg1_qqE521yYqfFHCos2nasMg8cvyjCicJ1YokAq69WuTIZsP_uTo4ARUjebCF5cgAhFGRpb367_RFhWd-YM8-zCzaO7tCHzSIGEa655kcF1afLu-FY_xhmhPMhEIqnsWxszRpZhwbANGH1sVKOwR60f43BG1k30iAIQi6ONK72XdSABUCrJJqGBlZxM9aX0vCbJezBIV_MvkZTVYjtz6ENUD52RP8MzKeMujVHPQKE81Q
As a "pure hack", I added this header to an image fetch, and SUCCESSFULLY GOT THE METADATA. As suspected, this code is VERY time limited (I suspect an hour), but can be used on ANY fetch.
I ALSO tested security rules (changing which level of the hierarchy was allowed, whether an entire tree was allowed or not, whether authentication was required - AND THE RULES APPLIED TO THESE FETCHES.
When we observed the URLs returned by .getDownloadURL()
we see that TWO parameters are added - alt=media
and token={uuid}
. I've been doing this a LONG time, so I tried to add JUST the alt=media
parameter. And...
Short-lived joy, however, because I just manually copied the authorization token. What I have proven is that there is a way to fetch images that follows the security rules, but I don't believe it is usable (yet) in-the-wild - I'd need to find a way to access that authorization token, or have a function from Firebase Storage that gives me such a token.
Security-rule guarded, SHORT-TERM access to images from the tag in the browser.
const authorization = "{Big long string I copied from a different image fetched via above-mentioned code}"
const aspectRatioBorderedImage = {
position: "absolute",
top: borderSize + units,
left: borderSize + units,
width: imgCalcWidth + units,
height: imgCalcWidth + units,
borderRadius: "50%"
};
const errorHandler = async (e) => {
e.stopPropagation();
const target = e.target;
const src = image; //$(target).attr("src");
const options = {
headers: {
authorization: authorization
}
};
console.log("errorHandler", src);
await fetch(src, options)
.then((res) => {
console.log("res", res);
return res.status === 200 && res.blob();
})
.then((blob) => {
if (!blob) {
console.log("res error");
return;
}
console.log("URL", URL.createObjectURL(blob));
$(target).prop("src", URL.createObjectURL(blob));
});
};
...using JSX...
<img
src={image}
crossOrigin="use-credentials"
style={aspectRatioBorderedImage}
alt="good try"
onError={errorHandler}
/>
Google Cloud Storage https://cloud.google.com/storage/docs/authentication/signatures
Security rules are primarily aimed at use with SDK calls (as you noted, get()
, getBytes()
, and the corresponding write calls). Download URLs are intended to be static and work without rules, because they are based on the underlying GCP service without Firebase wrappings. I'd take this a step further and say that the primary reason to have request.auth at all is to get to the custom claims, which allow you to enforce some primitive role-based access scenarios, admin privileges, and similar based on user accounts.
You can of course drop down to the Google Cloud Storage REST API and use that with Authorization
headers and that's a documented workflow (in GCP). You can also work with IAM permissions here rather than security rules if you wish.
There's certainly more that we can do here in the future to fill out the security rules story (such as some day allowing you to do get() requests against your Firestore data to help with roles and groups), but functionality is limited in scope for now as described here.
Yes, that is the reason I want to have the Rules apply - but FIRST the rules have to be enforced at all. I have gone into the code to see exactly how your code creates the headers, and it now seems apparent that you are using the JWT
returned from auth.getIdToken()
as the signature (I can point you to the line in the code, if you want). Given that, I am now writing my own code to do the same thing as an experiment, then build it into my own wrapper package (open-source and public).
The Google Cloud Storage REST API doesn't seem to have the same hierarchical Rules structure that Firebase Storage does - but since your code can follow the rules, mine can as well.
Not the most straight forward, due to browser limitations. It is NOT possible to add custom headers directly to <img>
tags, but it IS possible to set an onError
handler to do an independent fetch.
<img
src={image}
//crossOrigin="use-credentials"
style={whatever}
alt={whatever}
onError={errorHandler}
/>
where:
{image} is the Firebase Storage URL, which can be entirely programatically generated.
NOTE: the URL MUST include the alt=media
tag (or whatever is appropriate to the file type). If this parameter is missing, Firebase Storage will return the metadata JSON object.
{errorHandler} see below
//built in React
const [authorization, setAuthorization] = useState(null);
//Asynchronously fetches users JWT. Note closure, as useEffect itself must be synchronous
useEffect(() => {
if (user) {
(async () => {
const authToken = await fetchJWT();
setAuthorization(authToken);
})();
}
}, [user]);
//Technically, this is in my wrapper library - uses the Firebase Auth service to fetch a user's JWT
export const fetchJWT = async (user) => {
const thisUser = user || FirebaseAuth.currentUser;
//the "true" below forces a reset
const JWT = await thisUser.getIdToken(true);
return JWT;
}
//More-or-less copied from firebase-js-sdk/storage/src/implementation/requests.ts
const addAuthHeader_ = async (headers) => {
if (authorization !== null && authorization.length > 0) {
headers["Authorization"] = "Firebase " + authorization;
}
};
// handles the actual error event
const errorHandler = async (e) => {
e.stopPropagation();
const target = e.target;
const src = $(target).attr("src");
let headers = {};
addAuthHeader_(headers);
const options = {
headers: headers
};
await fetch(src, options)
.then((res) => {
return res.status === 200 && res.blob();
})
.then((blob) => {
if (!blob) {
return;
}
$(target).prop("src", URL.createObjectURL(blob));
});
};
when the <img>
tag encounters an error, the errorHandler
src
attribute from the event target. options
object for the fetch()
call, including headers. The headers get an authorization
header added, which consists of a string with `Firebase ${authortization}`
URL.createObjectURL(blob)
, which is then set as the src
for the target <img>
.A file (in this case, an image) is fetched from Firebase Storage, following all of the Security Rules set therein. No permanent token needed. Only authenticated users have access. (I will note that I use Anonymous User accounts for new users, with appropriate restrictions set on their access)
I am likely to added a class to <img>
elements that are intended to show images from Firebase Storage, or possibly examine the URLs. That way a single errorHandler can be designated to handle all of them.
GOOGLE should set Firebase Storage to allow the use of Credentials, which CAN be added to both <img>
tags and background-image-credentials
properties.
an extremely simple .getPrivateURL()
that at least created the URL without the token, which could then be used with the JWT token (mentioned above) without having to re-create the full path construction (including the bucket domain, the bucket name a top-level "path", and URL-escaped (%2F-instead-of-/)
filePath.
Although I suppose I could just strip the silly token. The problem is that would be on the client side - I would rather the token never be on the client side. Same issue for using .getIdToken()
- it brings the token to the user side - but as you may have noticed up there in various places, said (very short lived) token is already present on the client side.
MUCH MUCH MUCH BETTER to allow the use of the credentials, verified on the SERVER side.
I'm facing the same issue, I want urls to my files saved in firestore for quick access but I also want them to respect the rules I wrote for the files, right now if I generate a download URL it bypasses the rules, this means that the entire system is useless, might as well remove rules.
Environment
The problem
So, Google Folk:
Would someone please tell me under what conditions would any authenticated rule ever be passed for reads in the Firebase client SDKs?
For Example:
It seems obvious enough - until you finally realize that the client SDK's failed to implement the (extremely obvious) .get() method on a storage reference.
.getDownloadURL()
creates a TOKENIZED URL, which BYPASSES the above security rules.Use of an UNtokenized URL in a browser context (such as an image URL) DOES NOT INCLUDE authentication headers, so the above rule will ALWAYS fail.
IF there is no
.get()
method, and... IFgetDownloadURL()
always generates a URL that bypasses the rule...What the ever-loving-heck is the POINT of such a rule?
And PLEASE don't repeat the usual pushback of "once a URL is in the world what's the point of security" - I have very good reasons (which I can explain in confidence, out-of-band) why I want this level of control.
It's ALSO clear someone INTENDED there to be this level of control.
I can, of course, write a wrapper that some-way-or-another gets the authentication headers and add them to an HTTPS request (SPECIFICALLY WHAT YOUR CODE IS DOING ALREADY) to download an item (whether image or not) to the client JS...
But WHY would I do that when I can see that virtually ALL of the needed code ALREADY EXISTS in the SDK, you just didn't expose a
.get()
method...so: either some way to cause browser image fetches to include/use the cookie for authentication (is it a CORS policy?) OR just simply add the
.get()
method to StorageReference in the client SDK(hey, put me on contract, I can probably get it done - I've already started poking in the repository, which is why I know most of the code structure is there)
Steps to reproduce:
(FYI, I am creating a @leaddreamer/firebase-wrapper library for my own use, which INCLUDES wrapper to make the client API almost identical between client and Admin SDK. Obviously the
.get()
is trivial in Cloud Storage. It doesn't help the browser, though)