Is Bruno any good for API hacking?
That’s been a question on my mind lately. For a couple of reasons.
First, looking deeper into Bruno has been on my to-do list for some time now. The community has been discussing it for a while, and I have only looked at it to get familiar with what it did.
Second, I was recently asked on Twitter if I would write about Bruno.
It seems like a good time to try Bruno and see how well it works for API security testing.
So join me as I experience Bruno for the first time by trying to port a Postman collection full of security tests into Bruno and get it working in my Azure DevOps pipeline to work as an automated guardrail against branch merges for a sensitive API.
So, before we go deep into using Bruno, you might be wondering why I’m even looking at this.
It’s simple, really.
I’ve Postman for many years. However, the company has made some recent decisions that I have not been a fan of.
Since around v10, Postman can no longer run completely offline. In fact, you are forced to log in to the Postman cloud service to use the client and host your tests in workspaces that are synced to their cloud.
You can’t keep sessions offline, nor can you protect sensitive tests and their payloads away from Postman. Hell, you can’t even access their Security & Trust reports without having to log in. It’s just ridiculous.
We continue to see more informational data and credential leaks from Postman collections because of workspace mismanagement and misconfiguration. In fact, people are demonstrating Postman dorks that allow you to search the public Postman API Network and detect these security risks.
This is where Bruno could possibly help by eliminating these risks.
What attracted me to Bruno was their manifesto.
Specifically, it was this section:
With Bruno, collections become first-class citizens, co-located with related information and easily version controlled. We say goodbye to bloated workspaces and global environments, and embrace the benefits of co-location.
We dream of a world where developers can clone a code repo, get it up and running, use Bruno to browse the examples on how to use the API and start playing with it.
I mean, doesn’t that sound EXACTLY like some of the pain I am having with Postman collections for sensitive targets? It moves the collections locally and allows me to control them in my own source control instead of relying on syncing to workspaces in Postman’s cloud.
They do this by introducing the BRU Markup Language.
Bru is a simple markup language with JSON-like semantics. This makes it easy to construct and modify API requests programmatically.
For some time now, I have been thinking about ways to help the API Hacker Inner Circle community automate the scaffolding of real-world security tests. This would include effective payloads based on real traffic flow that can dynamically generate API specification docs along with corresponding security tests.
If you don’t know what I am referring to, check out my article on How to craft rogue API docs for a target when they don’t exist.
If Bruno’s manifesto aligns with its work on its markup language, this might be helpful tooling.
But is Bruno up to the task? Let’s find out.
I won’t waste your time explaining how to download, install, and set up Bruno. You can figure out how to download it here.
When you open it for the first time, it will feel remarkably similar to the Postman desktop client. Actually, it feels better, as there is far less clutter.
The familiar layout of this Electron app makes it easy to get started. Let’s quickly create a new collection.
With the new collection created, we can click the three-dot ellipses and create a new request.
And wow… does it not look and feel like a typical API client like Postman?
I won’t spend much more time going through all the tabs and whatnot. If you’ve used Postman before, you will know how to navigate this layout.
So let’s move on and see how easy it is to migrate an existing Postman collection into Bruno.
On the main desktop of the Bruno app is a link to “Import Collection”. When clicked, it offers you the ability to import a collection from several sources, including:
This is impressive. It helps to reduce friction from existing API clients as someone migrates to Bruno. I even liked seeing that it would try to convert my tests into Bruno-compatible tests.
Let’s give it a try with a simple Postman collection I have of security tests for crAPI.
So right away I immediately saw a blocking issue. During import, Bruno took the Postman collection and sorted the folders alphabetically. I am guessing this is because it relies on the folder structure it creates on disk.
This is a real problem. Similar to how the Postman Collection Runner works, Bruno’s Collection Runner executes its tests in top-to-bottom order. However, because it sorted the folders alphabetically, the order is broken on import.
I always order my tests in order of precedence so that previous test scenarios can influence later tests by generating test data in a way I expect. This is completely broken on import.
My workaround was to rename each folder using a number schema so that they would be in the correct order. However, this could be cumbersome on my production Postman collections, as some projects have over 100 folders to represent test scenarios, with dozens of tests in each folder.
During the import, the defined Collection-level variables were ignored. Even though Bruno supports the concept of Collection Variables, it does not import them at all. I was forced to recreate all the variables manually in a Bruno environment. (More on that later)
What’s worse, Bruno does not currently have a UI for adding, editing, or removing Collection variables. You can only set them through code using bru.setVar()
.
Bruno gave an honest effort to convert the Postman tests. However, it could have done better. Fundamental stuff like converting over the Postman sandbox object in favor of Bru failed on basic things like the HTTP status code expected and the body JSON payloads.
I honestly can’t fault the developers for this. The checkbox said it would “try its best.” However, this can be done better, as much of this is standard language transformation stuff.
Let’s move on. Now that we have the collection imported, let’s go about fixing the broken tests.
So after the import failed to convert all the tests, I started debugging what was happening. The Scripting Javascript API Reference Guide helped.
The good news is that Bruno uses the same Chai.js Assertion Library as Postman. So, once I remapped the sandbox objects and data access, most of the tests migrated over easily.
Simple examples include changing pm.response.code
to res.status
. More complex examples started to happen when you realized that Bruno mucks with the actual response headers being returned.
I couldn’t immediately figure this out. So, I set up Bruno to proxy its requests through Burp Suite so I could debug this further, which led me to another issue.
Unlike Postman, which allows you to configure the proxy to use multiple protocols, the Bruno UI currently only allows you to configure one at a time. So, mixed requests can cause issues. It wasn’t too much of a problem during this evaluation, as I selected APIs that used a single protocol.
However, if you have mixed requests, this could become an issue.
With Bruno’s requests proxying through Burp Suite, I could quickly compare and see the issue I was having.
I quickly realized many of my tests related to response headers were failing. As I investigated it further, I learned that calling res.getHeader('header_name')
never returned the HTTP headers I had expected.
The library that Bruno uses for HTTP request/response handling always lowercases the response header names. So, even though the web server may return “Server” (with a capital S), Bruno always returns “server” with a lowercase first letter.
This breaks a lot of my tests.
But this isn’t their fault. In accordance with RFC9110, header field names should be case-insensitive.
Updating all the tests to use lowercase names solved the issue. However, Bruno’s docs should have called that out, as it specifically altered what was being returned from the server.
With my tests now fixed, it was time to fix the missing Collection Variables.
I am going to be honest here. I find the Bruno documentation for managing variables and secrets “lacking.” The terminology is mixed up, which can confuse people quite quickly.
So, let me clarify it for you.
There are Request variables. You can manage them in the Vars tab of a request. There is a UI for it within the Bruno app.
There are Collection variables. Once set, you can view them by clicking on the eyeball at the top right of the Bruno app.
You can not manage Collection Variables through the Bruno app. You can only alter them programmatically with bru.setVar()
and bru.getVar()
.
I’d show you what it looks like, but at present, a bug exists where if a collection variable is set, the Bruno app will crash with a white screen if you try to view it.
There are Environment variables. These are NOT to be confused with dotenv environment variables, but with “Bruno Environments.”
You can configure an Environment by clicking the drop-down on the top left of the Bruno app and selecting “Configure”.
You will be prompted to create or import an environment, and to name it. Once complete, you will have a rich UI that allows you to add, update, and delete variables that can be used within an Environment.
You can access these variables through the UI and manage them programmatically. You can use bru.getEnvVar()
and bru.setEnvVar()
to accomplish that.
One of the more interesting things about Environment variables is that you can mark a variable as a Secret Variable. This will extract the value from the base configuration, encrypt it, and store it in accordance with your OS’s security. On the surface, this is awesome. However, as I would later learn, this design limits its functionality and useability. More on that later.
Last but certainly not least are dotenv variables. These are variables that can be stored in a .env file in the root folder of the collection. Then they can be fetched using a syntax of {{process.env.VariableName}}
.
You can combine this with the Environments in Bruno to separate sensitive data. This also allows you to add .env files to your .gitignore to make sure you never check in sensitive secrets and keys.
There is no UI to edit or modify .env variables. However, you can programmatically access them using bru.getProcessEnv()
.
So let me get back to the whole conversation about the value of using Secret Variables. It is awesome that they are properly encrypted for the user through the Electron app. However, this comes at a cost.
You can’t access these secrets outside of Bruno. This means that if you want to run your security tests automatically at the command line, like through the bru cli, it won’t work. It also means if you use Secret Variables, you cannot use Bruno tests in your CI/CD pipelines.
This means I can’t use this in Azure DevOps, which is a blocker for me. So, setting up all the Secrets in the Bruno Environment is moot and a waste of time.
The Bruno dev team recommends passing sensitive variables in through the command line parameters (not an option for me) or using .env files if this is an issue.
I decided to investigate where Secrets were stored and how Environments were managed. What I quickly learned was that:
I changed the permissions to reduce access and confirmed that the application continues to function. However, if I were to store sensitive data in .env files, I would further reduce the permissions to only be readable by the owner (400), especially in the CI/CD pipelines.
With the collection imported and reordered, tests fixed, and variables configured, it was time to run everything and see how it went.
Overall, it went well. Everything ran as expected, and it ran pretty fast. Of course, when I tried to rerun everything with the bru cli, I quickly came across the blocker where using Secret Variables failed to let the tests run.
Then, I tried a more complex run in which I wanted to inject an external collection of malicious input payloads to break the API using naughty strings.
This simply couldn’t be done.
I expected that the Collection Runner in Bruno would work the same way as Postman, where you can load an external file to inject data for each iteration of a run. However, that is not the case. It is documented as a feature in the backlog. In fact, if you look at the Pricing page, this is apparently a feature available only in the paid version and is coming soon(ish).
Even with all the issues I have been finding, I like the premise of Bruno. At this point, I decided to buy the Golden Edition and support the project.
So, what else could I do in Bruno now that I had a “Golden” license?
One of the paid features of the Golden license is access to the OpenAPI Designer and the ability to import API specs.
So I gave that a try.
And it failed.
So, pretty much all my OAS3-compatible spec docs are in a JSON format. Every single one of them I tried to load failed. No warnings. No errors. There is no output at all. I simply had no idea why it wasn’t working.
Then, someone on Discord pointed out the issue. Bruno only supports importing YAML-formatted OAS3 docs. *sigh*
As a final comment, let’s explore Bruno’s general usability.
Overall, the Bruno app is quick and responsive. However, I’d like to see several UI enhancements. And this is probably because I have been spoiled with Postman’s UI, which has had years to iron this out.
Here are just a few examples:
So, to this article’s original question and purpose… is Bruno a good Postman alternative for API hacking?
And would I use it in production?
I want so much to say yes, as I love what this open-source project is all about. I like their manifesto. And I love that everything is saved and run locally.
But based on my experience of using it… no, it’s not ready yet.
It’s just not mature enough. But it may be in the near future.
But I also have other concerns.
Like any open-source project, taking a dependency on it comes with risk. The long-term sustainability can be questioned as volunteers who work on the project get burnout from working on it outside of their day jobs.
Bruno is no different. The project leadership has already mentioned that they don’t want to seek outside investment to fund much of the work and are relying on the Golden Edition to offset some of this. So, a gap in funding leaves work to the community of volunteers. This is normal for new OSS projects.
Its popularity, even to this point, has overwhelmed the Bruno team, flooding it with almost 800 open issues and over 160 pull requests that are being ignored. Some of the pull requests are over seven months old, and I expect we will see collisions and conflicts because they aren’t being managed well.
It’s gotten so bad that forks have already emerged from people who want to depend on Bruno at work but need things fixed sooner. This one from Its-treason is worth watching. His fork is over 166 commits ahead and includes several fixes to issues I’ve encountered. It’s really too bad to see his work not getting merged into Bruno’s main trunk.
This is all solvable. The project leadership needs to expand the authorized contributors who can triage issues, groom the backlog, and merge the pull requests. This would offload the burden from just a few people to a bigger community of volunteers who want to see Bruno succeed.
I want to see Bruno succeed. I have reported every key issue I encountered to the Bruno dev team through GitHub. Below are links to many of the issues.
Over time, I expect most of these issues will be addressed. You can verify it yourself by checking the status of each issue through this list:
With any luck, I will revisit this article in 3 to 6 months and see that most of these issues have been addressed. When I do, I will be sure to update the article and strike out items that get fixed.
Bruno has a lot going for it. I can’t wait to see it mature enough to use it for production-level API security testing.
Until then, I will continue to support the project by purchasing Golden Edition licenses and offering my feedback as I examine it periodically.
For now, I will continue to rely on the Postman Collection Runner to run my test suites.
I encourage you to try Bruno for yourself and see if it meets your needs. Support the project in any way you can. It’s worth seeing it succeed.
Have you joined The API Hacker Inner Circle yet? It’s my FREE weekly newsletter where I share articles like this, along with pro tips, industry insights, and community news that I don’t tend to share publicly. If you haven’t, subscribe at https://apihacker.blog.
The post Is Bruno a good Postman alternative for API hacking? appeared first on Dana Epp's Blog.
*** This is a Security Bloggers Network syndicated blog from Dana Epp's Blog authored by Dana Epp. Read the original post at: https://danaepp.com/is-bruno-good-for-api-hacking