Shipping secure features without slowing down
The delay teams blame on security is usually rework. Here is where we spend the effort, and the half hour at design time that saves most of it.
Teams that describe security as the thing slowing them down are usually describing rework.
A feature ships. A fortnight later a penetration test finds that one customer can read another customer's documents by changing a number in a URL. Fixing that properly means changing how the data layer decides who is allowed to see what, which touches every screen built on top of it. Those two weeks are the price of building on a foundation nobody thought about, and the security work takes the blame for them.
The conversation that would have prevented it happens at design time, costs half an hour, and needs no specialist in the room.
Four questions, before the schema
Who is allowed to do this? Naming a role is where the discussion usually stops, and the question underneath it is which specific rows. A relationship manager can see their clients' accounts. Which relationship, recorded where, checked by what?
What happens if the request arrives twice, or out of order, or from somewhere unexpected? Payments get submitted twice by impatient people on bad connections. If the answer to a duplicate submission is two transfers, that is a design problem rather than a bug to find later.
What is worth logging, and what must never be? A transaction reference belongs in the log. A card number does not, and neither does the full request body that happened to contain one.
Then the uncomfortable one: what is the worst thing this feature could be used for by somebody determined? Not to solve it in the meeting. Just to find out whether the answer is embarrassing or expensive.
Authorisation is where the bugs actually are
Encryption gets the attention. Authorisation is where the real problems live, and they look boring in code review.
Here is the pattern that causes most of them. The interface only ever offers a user their own records, so the query behind it is written to fetch a record by id. The check that the record belongs to the caller lives in the screen that built the link, and the endpoint trusts it. Then somebody calls the endpoint directly.
The fix is structural rather than a matter of being careful. Authorisation belongs in the layer that fetches the data, so a query that skips it cannot be written in the first place. Row Level Security in Postgres does this well: the policy sits on the table, and a query written by a tired developer on a Friday is subject to it in exactly the way a careful one is. This site's own database works that way, which is why the contact form's table has no read policy at all.
Where that is not available the same idea applies by convention. One function that takes the caller and returns the records they may see, and no path to the table that goes around it.
Three more habits catch a large share of what remains. Secrets never go in the repository and never get inlined into a client bundle, because anything a browser can download is public whatever it is named. Dependencies are updated on a schedule rather than in a panic, since a framework four major versions behind is not a task but a project, and it always arrives in the week a published vulnerability forces it. And input is validated on the server even when the form already validates it in the browser: the browser check exists so people are told about a mistake immediately, not to keep anyone out, because anybody can post to the endpoint without loading the page.
What a review asks
Before a feature goes live it gets read against a short list, deliberately short enough that it actually gets used.
Can this endpoint be called by someone who is not signed in, and what happens if it is? Does every query that returns a record prove the caller may have it? Does an error message tell an attacker something a user did not need to know, such as whether an email address is registered? Is anything sensitive being logged? Is there a rate limit on anything that sends an email, creates an account, or costs money to run? Does the feature still behave if the same request arrives twice?
Most reviews find nothing. The ones that find something usually find it in the first two questions.
Regulated work adds a requirement that engineering discipline alone does not cover, which is being able to show afterwards what happened. That means an audit trail written as part of the transaction rather than logged alongside it, so it cannot disagree with the record it describes. It means knowing which events have to survive a database restore. And it means strong customer authentication designed into the payment flow from the start, because retrofitting a second factor into a checkout that assumed one is close to rebuilding the checkout.
None of this is slow. All of it is slow to add later.
A team that thinks about authorisation before writing the schema, keeps its dependencies current and runs a short review before release does not ship more slowly than one that skips all three. It ships without the fortnight of unplanned work that lands with the test report. The fastest projects we have run were not the ones that cut this out. They were the ones that never had to stop.