Computer Science Educator at CSUMB. I teach people how to contribute to open source projects.
This article is part of a series of case studies exploring the skills of senior software developers . In each case study, we look at a specific challenge faced by a senior developer and investigate the reasoning the developer used towards finding a solution.
In this case study, we will understand why Dale McGrew, the CTO and co-founder of We Vote, chooses to prioritize security above all else, and we will investigate how he executes this priority in practice.
We Vote is an open source web and mobile application that aims to make it easier to vote based on your values. After inputting your location and issues you care about, We Vote analyzes all of the available data (candidate records, propositions, etc.) for the upcoming elections and scores them based on your values so that you don’t have to spend 12 hours reading the voter guide yourself.
“It is a privilege to be able to provide software for a community of people”, says Dale. “You must be sure that you are providing your software in a way that keeps them safe. If it isn’t safe, you must drop everything else to make it safe.”
Dale lists his priorities in the following order of importance:
- Protecting against data damage or data exposure
- Keeping your servers up
- Protect your brand
- Everything else
This makes sense to me from the user’s perspective. As a user of Facebook, for example, do I care how “maintainable” or “scalable” their codebase is? Not really. Do I care about whether they are keeping my private messages safe and secure? Definitely.
So I think we can all agree that safety is important, but how serious is the threat?
Based on Dale’s prior experience working at tech companies, the threat is quite real: “I have seen hacking attempts in server logs at companies where I have worked, where someone was systematically testing for any kind of system reaction that they could exploit.”
What is the impact of this threat?
Dale: “Security breaches can damage your data, bring down your system, and affect your users’ experiences. It is no fun having to fight back against a hacking attempt at 5am, before you have even had coffee. It feels a lot better to secure your software and data on your own terms and in your own time.”
Introducing the API: Gatekeeper of user data
We Vote is a web application. Let’s walk through the workflow of how information is retrieved on a typical page of We Vote, and then we’ll dig into what the “API” is and why it plays such a critical role in keeping the users’ data safe.
Let’s examine what happens when we go to a typical page on We Vote’s website: https://wevote.us/ballot. Here’s what we see:
Note that after I put in my address, We Vote populates the page with the specific candidates who are running for office in my area. This is of course very useful information for me to know, but where did this info come from?
The names “Jimmy Panetta” and “Jeff Gorman” are not stored on my computer anywhere. No, they are stored on We Vote’s computer (specifically inside of We Vote’s database). When I visit wevote.us/ballot, my computer (the client) makes a request to We Vote’s computer (the server) for this information.
The internet is basically a bunch of computers that talk to each other. One computer (the client ) will send a request to another computer (the server).
Let’s take a closer look at this request. We can view the exact format of the request by using the Chrome browser’s inspector:
Note that the browser is sending requests to the following URL: https://api.wevoteusa.org/apis/v1/voterBallotItemsRetrieve.
The server then sends back the following response.
Thus, we see that the ballot information contained on the page ultimately comes from a request to the /voterBallotItemsRetrieve url. In fact, there are even more requests on the ballot page that are being made to other url’s:
The following URL’s are being hit, for example, to display your account settings:
The following url is hit to show your friends list:
The key point here is that in accessing a single page of We Vote, dozens of requests are made to other urls that look like https://api.wevoteusa.org/apis/v1/… Each of these “API” urls fulfills a specific function and generates a specific JSON response.
The dozens of API urls are in fact referred to collectively as “the API”, and each individual url is referred to as an “API endpoint.”
Note that the API endpoints are not intended for general public use, because they return JSON data — not the most human-friendly format for viewing data. These url’s are intended for the browser (the client) to use in order to generate the HTML/CSS page.
Nevertheless, even though the API endpoints are not intended for public use, it is possible for any member of the public to use them. All we would need to do is copy and paste one of the API url’s into the browser and voila — we can view the JSON data!
The most sacred property owned by We Vote is the user data (names, phone numbers, email addresses, political affiliations, friend lists). You can imagine that the API endpoints are the “doorways” to access this data.
What a malicious hacker might do
There is one potentially dangerous implication of exposing an API url for public use. The structure and format of the url — specifically the url parameters — reveals something about what the server code expects and how the server code is written. A hacker may then attempt to tweak the parameters in various combinations to try to manipulate the server’s behavior.
Note the structure of the full request to the /voterSMSPhoneNumberRetrieve endpoint:
After the “?” symbol, we see all of the url parameters, which in this case is exactly one:
- voter_device_id = …
The voter_device_id field uniquely identifies me as a user of We Vote. What would happen if I were to tweak the voter_device_id? If I were a malicious hacker, I might try to guess the voter_device_id of another user, thereby gaining access to their email address, phone number, voting preferences, political affiliation, and friends list!
I might also try to leave out the voter_device_id altogether. What would the API return then? Maybe I could get lucky and somehow the API would return all of the voters in We Vote’s database! What a treasure trove of information that would be.
As Dale mentioned earlier in the article, it is not uncommon for hackers to systematically probe every nook and cranny of the API to find some kind of vulnerability. Thus, it becomes imperative that we make sure that the API url’s are secure.
We have already outlined two “nooks and crannies” that a hacker might try to exploit:
- Call the API endpoint with random voter_device_ids
- Call the API endpoint without any voter_device_id at all.
In both of these cases, the hacker’s goal is to get access to the the private data of other We Vote users. These are in fact the two primary cases that Dale focuses his security efforts on, and in the next section, we will investigate the specific strategies he uses to safeguard the API.
How Dale protects the API
“All of our API’s that deal with private data have code that stops execution of the code if a valid voter device id isn’t found,” says Dale.
In the We Vote codebase, there are two defensive strategies that Dale uses to ensure that the API keeps users’ data safe.
- The code itself contains numerous checks to ensure that each API call includes a valid voter_device_id
- A suite of unit tests continuously probe the API to verify that the API behaves correctly when an invalid voter_device_id is passed.
Let’s examine strategy #1 first. In order to understand how the codebase itself is designed, let’s start with a bird’s eye view of the We Vote architecture. The codebase is divided into three primary code repositories:
- WeVoteServer → The code that runs in the server (We Vote’s computer). This is written in python and uses the Django server framework.
Question: Out of these three codebases, where do you think Dale focuses his primary security efforts? (Hint: Where do you think the API logic is written?)
WeVoteServer implements the logic for each of the API endpoints we discussed above:
Let’s dive into one one of these API endpoints: /voterEmailAddressRetrieve. We’ll follow the workflow of what code gets executed, and we’ll identify the point at which Dale includes a defensive security check.
If you are curious to follow along yourself and find the exact files we are referring to, you can find the API code in the apis_v1 folder:
We have already identified the first two steps in the workflow:
The third step takes place in the python logic on the WeVote server:
3. In the apis_v1 folder, we have a file named urls.py. This file routes the incoming request to the views_voter controller
4. Inside the views_voter.py controller, we find the function voter_email_address_retrieve_view that performs the business logic:
On line 757, note that this function extracts the voter_device_id from the incoming request and passes it along to a helper function: voter_email_address_retrieve_for_api
5. The voter_email_address_retrieve_for_api function lives inside the email_outbound controller:
It is here (line 748) that the codebase performs the defensive check for the existence of a valid voter_device_id. The function follows these steps:
- If there is no valid voter_device_id, return an error response (lines 749–757) and an empty email address list.
- Query the database for the voter information (line 760).
Note that step 1 happens before step 2. If there is no valid voter_device_id, the code exits the function early, and we never even get to step 2. Thus, the database is never queried and is kept safe.
Dale designed the API logic to follow this pattern for each and every endpoint. Before any query is allowed on the database, a defensive check is performed to ensure that the incoming request includes a valid voter_device_id.
If the API endpoints are the doorways to We Vote’s user data, then these defensive checks are the locks on each doorway. The key to each lock is a valid voter_device_id.
Unit tests for the API
In addition to the defensive checks in the code itself, as outlined above, Dale added another layer of defense: a suite of unit tests that continuously probes the API endpoints and verifies that they behave as expected.
“I would encourage developers to set up and learn a testing framework first thing,” says Dale.
The We Vote server uses the django framework for python, which offers a formal testing library django.test. Let’s examine how Dale uses this framework to write a test for the /voterAddressRetrieve API endpoint.
Note that most of the file has been stripped down (as indicated by the “#…” comments) for the purposes of this article so that we can get a higher level view of the design of the tests. There are two major functions here:
- test_retrieve_with_no_voter_id tests the case where the request does not have a valid voter device id. In this case, the test verifies (on line 15) that an error message is returned.
- test_retrieve_with_voter_id tests the case where the request does supply a valid voter device id. In this case, the test verifies (on line 28) that the correct address is retrieved.
Let’s take a closer look at the actual test (not the stripped down version):
On line 20, the python logic makes a request to the /voterAddressRetrieve endpoint. Line 21 converts the JSON response to a python dictionary. Lines 29–36 verify that response contains the appropriate error status: VALID_VOTER_DEVICE_ID_MISSING.
Many of the critical API endpoints have similar tests. There are 15 such tests unit tests inside the apis_v1/tests folder. Each file is responsible for testing a specific API endpoint:
To continue our metaphor: If the API endpoints are the doorways to We Vote’s user data, and if the defensive checks in the code are the locks on each door, then the unit tests are like the security guard that double-checks that the doors are in fact locked.
Junior Developer vs. Senior Developer
One subtle point to notice is what Dale did not test for. There are a host of other cases (related to the user’s address) that are covered in the codebase:
- User has no address saved → API response should be empty.
- User initially saved one address, then later changes the address → API response should contain the newer address.
- User initially saves an address, then later deletes it → API response should be empty.
- and many more…
A less experienced developer might get lost writing a multitude of tests to cover all of these cases. Dale recognized that there are two primary cases to test for (request does or does not contain valid voter_device_id) that have a significant impact on the core business need: protecting user data. The rest of the cases, although important from a usability perspective, are not that important from a security perspective. Recall that security is Dale’s highest priority.
Furthermore, there are almost zero tests written for WeVoteCordova (mobile app codebase) and WebApp (browser codebase), the client-facing code. Nearly all the testing effort is in WeVoteServer. Why? Again, from a security perspective, the highest priority is protecting the API, because it is the most immediate public-facing layer that surrounds the user data.
In a bank, for example, would you expect the front-door or the door of the money vault to have higher security? The vault door! Because it is the most immediate layer that surrounds the money.
In an ideal world with infinite time, sure, Dale would have likely written tests to cover all of the cases and all the codebases. But in the real world, time is a factor. If Dale had focused more time on writing tests, he might not have been able to launch the app so quickly, and WeVote would not be the publicly available app it is today with over 50,000 registered users.
Disclaimer: I am not saying that writing more test cases is bad or that writing tests for the client code is bad. More testing and more thorough testing is generally a great idea. What I am saying is that if you have to prioritize which tests to write first, focusing on the API as Dale did is something we can all learn from.
Dale’s awareness of the business needs and priorities informs the way he writes code. This is one of the key skills possessed by senior software developers that separates them from more junior level developers.
Call to action
First, if you were at all confused about who / what to vote for in the last election, We Vote does a fantastic job of helping you navigate through the confusion quickly and easily. Download the app for ios or android or use the website.
Second, there are many opportunities to contribute to We Vote as a developer. If you’re not sure how to get started, I am creating a course that teaches you how to set up the codebase, get familiar with the architecture, and contribute to the We Vote open source project. It guides you towards putting the skills discussed in this article into practice.
Sign up for the course here.
- Dale McGrew for answering my numerous questions while researching for this article.
- Jenny Tse for introducing me to Dale and helping me host the podcast with him.
- Tom Lee and Eduardo Soto Rodriguez for pair programming with me to write unit tests for the We Vote codebase.
- Suyash Joshi for his feedback on the first draft of this article.
- Quincy Larson for his feedback on the initial direction for this article.
Create your free account to unlock your custom reading experience.