Bits of your Bubble app you might not know are public

I wanted to write something up for people specifically looking to know how deep someone can dive into your app. Let’s poke around the https://bubble.io app to find out. This isn’t a post to attack Bubble, but to educate people as to what’s actually shared by Bubble to the end user as it isn’t really talked about in most developer circles, and whether you like it or not, this is how it is so be aware of it. This list isn’t exhaustive (you can also see plugins, the Swagger file is enabled by default etc) but some cool stuff to find here and any additional insights in the comments are appreciated.

Meta

Every app has a Meta exposed. You can find it at https://bubble.io/api/1.1/meta (replacing bubble.io with your own domain or the yourapp.bubbleapps.io domain).

GET

This is a list of all data types exposed by your Data API. These are protected by privacy rules. If you do not have a privacy rule configured on a data type, you can simply visit https://yourdomain.com/api/1.1/obj/datatypename in your browser to view all of the data available. Data API exposed + no privacy rules is the death combination. Data types will only show up here if the data API is enabled for them in the API settings of your app.

POST

These are your backend workflow endpoints that have ‘Expose as a public API workflow’ checked. It details the parameters the endpoint takes, and whether authentication is necessary. If you check ‘This workflow can be run without authentication’, then this will reflect in the meta page as “auth_unecessary”: true. Let me repeat that - anyone can visit your meta page, Ctrl+F “auth_unecessary”: true and be shown all of your unauthenticated backend workflows. Anyone can call an unauthenticated backend workflow without restriction. Heck, all of Bubble’s Stripe workflows are accessible without authentication (that doesn’t necessarily make them insecure - see this thread). But this public information gives attackers information to play with if you don’t secure them appropriately.

Takeaways

  • Do not leave endpoints unauthenticated unless you have another method within the endpoint that verifies the integrity of the request it is receiving
  • Avoid exposing the Data API on types you don’t need, so that if you make a mistake, it’s harder for someone to find out (this isn’t sufficient security but is a reasonable approach to minimise risk)
  • Configure privacy rules on all data types that you don’t want to be publicly viewable (duh).

Database

Your database structure is public, no matter how you set it up. It can be derived from console.log(app). The actual data (not the structure) can be protected by privacy rules. You can of course work this out from the editor, but @rico.trevisan made a tool to generate a DBML file which can be used to generate a detailed database diagram for your app. That discussion is over here.

If you just want a brief look, here’s an expanded view of Bubble’s ‘negotiated price’ data type. This seems to be their Enterprise price tracking method. So, we can tell that DB things has (or had) an influence on price, and whatever the hell MUDV is. I have a hunch this data type is pre-pricing changes though, so maybe stuff’s changed. Whilst hardly critical, it goes to show you can see a fair bit about how a tool works and work out some useful stuff from that.

Takeaways

  • All field names can be seen publicly
  • All data relationships can be seen
  • Knowing the structure isn’t inherently insecure but can make it easier for an attacker to identify areas to try exploiting

Option Sets

All option sets are public, no matter how you set them up. All option sets are loaded on page load, even if they are not used. It can be derived from console.log(app).

Hey, it looks like Bubble is testing out different free trial lengths:

Aww, here’s Bubble telling us everything we can’t build:

Takeaways

  • Don’t store sensitive data in option sets.
  • Option sets are loaded on page load. Very few uses cases will fall into this category but don’t store oodles and oodles of data on them as it might slow down the page load.

API Connector

All API calls are public. Data within API calls can be protected. Anything that’s not a parameter with ‘private’ checked can be worked out by an attacker. In addition, the API responses when you initialise API calls are public, which can sometimes include sensitive data. @gaimed at Coalias developed a convenient tool to check through this all yourself which will show you more than I could here. Here’s some of what we can see in Bubble’s API (emails that were used during initialisation, for example).

App Texts

I’d hope most people knew this already, but any app text is publicly visible. Not much else interesting to share on that front.

Takeaways

  • Don’t store sensitive data in app texts.
  • Don’t store inappropriate texts in app texts that were intended to be development placeholders…

Pages

Every page is visible (even if it’s not ‘accessible’ or a sitemap isn’t expose). You can’t just make an unsecured page called admin13097193805715 and hope nobody finds it because you don’t expose the sitemap. Bubble sure has a lot of them!

Any additional contributions or clarifications are welcome :slight_smile:

If you’re interested, I’m doing audits of individual Bubble apps here. I’ve done 20+ audits now and almost all have at least one critical security issue (leaking database data) and major issues (exploitable API endpoints/logic etc). If I don’t find one critical or major security issue, I’ll refund your audit cost! All of the information needed to keep the information shared above secure is in the post, but if you want a second pair of eyes feel free to get in touch :slight_smile:

EDIT: Now I’m thinking about it, if you take ONE thing away from this post it’s that obfuscation / assuming a user won’t be able to wind something is not effective at all. Hopefully that was already well known among the community but this might help people understand why…

41 Likes

Great post!

I can’t fathom why Bubble is telling everyone what their trial lengths are; why not put it in the DB especially since the option set references the DB anyway? The only answer I can think of is that they are trying to limit their WU units! :stuck_out_tongue_winking_eye:


I see overbloated option sets (arrays of images, etc) all the time and think that many more devs have to be careful not to overload these (at least until Bubble sets it up that it only loads the OSs needed for the page).

PSA: Just to add to what George said in terms of a side-by-side comparison:

Databases Option Sets
Schema Visible Labels & Attributes Visible
Bubble plans on
hiding** schema
Labels & Attributes
will remain visible
Data can be private Data is Visible
Only loaded when
referenced on page
Always Loaded
on page load
Loading DB data
costs WU units
Loading Option Sets does
NOT cost WU units

**after an outcry; timing is totally unknown.
I’m taking any bets on this triple parlay:

  1. the database schema obfuscation will create errors and bugs (and DB errors are never small, inconsequential ones);
  2. these issues would have been caught with proper testing; and
  3. the DB schema obfuscation will occur without any communication or update and Bubble itself won’t be aware that it was released until the forum does their work for them and brings the bugs to their attention, informing Bubble that the feature was shipped.

Any takers?

Awesome post. What tool did you use to show the pages?

1 Like

console.log(app) in devtools and show %p3.

To do with POST:
image
I would assume that having run without authentication not ticked that it will be safe no?

I think I need to look up and lean what those 3 check boxes do properly o.o

Expose as a public API workflow means it can be called via the API connector / an external app. If this is unchecked, the only way this endpoint can run is with Schedule an API workflow inside your app.

This workflow can run without authentication means that when checked, anyone can run this API workflow without being authenticated (normally via an admin API token). This doesn’t always mean it’s unsecured. It’s like leaving no guard at the door of a bar. However, so long as you check someone’s ID when they order, it’s still secure because you can reject them at that point even if you have let them through the door. If you can authenticate the request within the workflow and terminate it if the authentication fails, it’s likely still secure.

Ignore privacy rules is self explanatory.

2 Likes

Hello @georgecollier

Thanks for your post.
I was wondering what is the best practice to get access or not to:

  • the admin page (where user as an admin can manage billing, his teammate roles etc)
  • the website admin page, where i can manage all the customer and see what is happening within the app.

For now I use the WF action Do When condition is true if the user is admin or not

Protect the page with a page load condition that checks the user’s role. Normally when I build apps, a User’s Role is an option set list (so a user can have multiple roles) even if it looks like they’ll only require one so that admin roles and any future ones can be added without reconfiguring lots of logic.

I can’t remember off the top of my head but some page load / only when condition is true conditions will run before the page is downloaded, and some after (different redirect types). You can experiment using a redirect checker.

This is why privacy rules are important - even if the admin panel can be loaded, that’s not a huge issue so long as no data is loaded that the user shouldn’t have access to.

The same rough logic applies to both of your admin page scenarios.

Ok I see, thanks for this fast answer.
I have done it in a similar way.

What is a redirect checker?

1 Like

This is deeply concerning.

Hi @georgecollier

What ever I do I always get this from redirect checker

Problems found:

  • You use a 302 redirect. This means, that the actually content is temporary not reachable and will come back soon. To use a 302 redirection for generally moved pages is a bad idea. Search engine bot might not follow it or handle it as temporary. For SEO this is also a bad idea, because no link juice will be transferred to the linked page.

I tested with different actions:

  • when page is load, condition : user is not admin
  • when user is logged out
  • when user is logged in, condition: user is not admin
  • when condition is true, condition: user is not admin

I am looking for a solution that returns a 401 or 404.
Any idea ?

1 Like

you don’t have control over the response of GET requests to your pages. The only option you have is to add a page load workflow that checks a server-side condition to get a 302 redirect to another page.

2 Likes

Hey,

Has anybody tried nulling the value of the app variable on page load or after a second for example?

I see it’s used as window.app in a few places in the code (and by plugins) but that’s during the initial load and initialization of other variables they might use.

@josh @emmanuel Could there be any consequences of nulling the app variable?

you can still derive the value of window.app by inspecting the loaded code. what benefit do you imagine to gain by overwriting the variable?

1 Like