Capa do artigo: Does Security Cross Your Mind When You Ship Code?

Does Security Cross Your Mind When You Ship Code?

When the subject is security in development, it is common to associate the beginning of the journey with adopting tools like SAST and DAST, which analyze code statically and test the running application looking for vulnerabilities. Or with creating specialized teams, like red team and blue team, called in at some phase of the SDLC (Software Development Life Cycle). These strategies exist for good reasons and a CTO needs them. But they arrive late if the software is born every day out of fragile decisions and practices. And when the organization expects a single team to own the subject, that team becomes a bottleneck, accumulating a backlog of problems that could never have existed in the first place.

Security is a quality attribute.

Just like performance, maintainability, observability and reliability, security needs to permeate development from story refinement through implementation, review, testing, deploy and operation. When a feature is estimated without considering validation, authorization, data exposure, flow abuse, traceability and behavior under failure, the team is leaving quality for later, with a high risk of financial and reputational damage (which also translates into financial damage).

And quality left for later usually comes back as an incident, a steep interest rate to pay.

Sometimes it comes back as a bug, sometimes as rework, sometimes as downtime and, in the most serious cases, as headlines. A publicly exposed security flaw causes a kind of damage that is hard to measure, which is the loss of trust. The financial impact can be calculated, the legal cost can be estimated, the technical effort can be planned. But the reputational damage, the sense of carelessness and the broken trust with customers, partners and the market are much harder to calculate and repair.

That is why, when refining a story, remember to reserve time to develop this capability. Do not treat security as an external checklist, something someone validates at the end, or a task that can be delegated to an AI, a skill, a plugin or a scanner. These tools help, but they do not replace awareness and technical maturity.

Use AI, use tools, use automation. Regardless of that, learn the concepts behind these decisions and validate whether what is being delivered closes the gaps that you, as a professional, need to guarantee.

When we talk about security, it is common to imagine cryptography, authentication, authorization, sophisticated exploitation or some obscure breach. Many flaws, however, arise for far less cinematic reasons. The absence of small decisions, lack of technical awareness and fragile choices repeated in the day-to-day of development.

Let's explore some of them.

The screen already validates.
The button is already hidden.
The user already has a token.
The user already has a JWT.

And everything seems fine, but unfortunately it is not.

A good part of the security flaws in applications happen when the developer entrusts the protection of the application to something that exists only to help the user experience. Frontend improves usability, a token identifies a user, claims carry context. None of these elements, on its own, should be treated as a secure system.

1. Validation "already exists in the frontend"

This is the silliest mistake on the list and more common than it seems.

The frontend limits the field size, applies masks, blocks invalid values, disables buttons, validates email format, restricts uploads and prevents wrong combinations on the screen. So it is assumed that the backend will always receive what the interface allows to be sent.

Except the backend does not talk only to a screen. And even if it did, that would not prevent a user from "seasoning" the data before sending it.

They can use Postman, curl, scripts, bots, integrations, malicious users, browser extensions and any client capable of assembling an HTTP request.

Keep in mind that validation in the frontend is experience and validation in the backend is security and consistency.

When the backend accepts anything because "the screen already handles it", the application leaves part of its security in the hands of the client. And the client is precisely the part you do not control.

The fix is simple. Every rule that protects consistency, permission, limits or integrity needs to exist in the backend. The frontend can repeat that validation to improve the experience and, ultimately, avoid unnecessary calls to the backend, but it should never be the only barrier.

This error pattern also has a name. In the CWE (Common Weakness Enumeration), the catalog that enumerates known software weaknesses, it appears as CWE-602 (Client-Side Enforcement of Server-Side Security), the weakness of entrusting the client with enforcing a security rule that belongs to the server. And the same classification covers the next mistake on the list.

2. Permission "managed" in the frontend

The second flaw is similar and far more disastrous.

The application hides the button from those without authorization, removes the menu from those without the right profile, omits sensitive fields, blocks the settings screen, changes navigation according to the user's role.

That is good for the interface. So let's repeat this article's mantra:

The interface takes care of the experience.
The security and consistency boundary lives in the backend.

If the API keeps accepting the calls, hiding the button only reduces the chance of a regular user clicking it. Someone curious can still call the endpoint directly.

The question that matters stops being "does this user see this action on the screen?" and becomes "does the backend reject this action if it gets called anyway?".

Can this permission be validated at the API Gateway? It can. In some scenarios, it even should. That mitigates costs and calls to the backend, centralizes policies, reduces improper traffic and blocks obvious calls, but it does not eliminate the service's responsibility. The service still needs to protect the rule it executes.

Because not every call necessarily goes through the path you imagined. There may be another consumer, another internal flow, another job, another endpoint, another integration, another service or simply a forgotten route. When security depends on a single external layer, any architectural deviation can turn into an exploitable condition.

Every sensitive action needs to be authorized in the service. Create, edit, delete, approve, cancel, query, export, reprocess, change permissions, generate a charge. If the action has impact, it needs to go through an authorization decision, regardless of who calls it.

This is a good point to bring a Zero Trust lens into development.

Zero Trust, in the context of software, goes beyond network, firewall, corporate identity or vendor buzzword. It is a building posture that avoids assuming trust because the call came from inside, because it went through the gateway, because a token exists, because the user saw a screen, because another service made the call or because "nobody should know this URL".

Each layer should validate what is relevant to its responsibility. The interface improves the experience, the gateway applies cross-cutting policies, the service protects the business rule and the database can reinforce isolation when it makes sense. Security is a property of the architecture, distributed across the layers. Robust architectures implement security controls in depth, reducing the dependence on any single mechanism.

The frontend may decide what to show.
The backend must definitely decide what to allow.

There is also an interesting approach when the architecture decides to take REST more seriously, HATEOAS (Hypermedia as the Engine of Application State). In the Richardson Maturity Model, which classifies REST APIs in levels of adherence to the style, the most mature level delivers, along with the data, the links and the actions available for that resource in that context.

Instead of the frontend "guessing" what can be done based on role, state or duplicated rules, the backend returns which actions are available. An order may come with links to cancel, pay, reopen or track, depending on the user, the order's state and the system's rules. The frontend presents based on what was sent.

This does not eliminate validation in the backend when the action is called, but it improves the design of the experience. The interface stops worrying about permissions and handling derived from data and state and starts reflecting decisions made closer to the rules. The backend guides what can be displayed and remains responsible for denying any invalid attempt.

3. Authorization done just by "having a token"

This point connects directly with the previous one. If the frontend cannot be the security boundary, the backend needs to know how to differentiate two things that many people mix up: authentication and authorization.

The user logged in, received a token and the signature checks out. Great. The system knows who they are, but it still needs to validate what that user can do.

Authentication identifies.
Authorization defines what one can see and do.

The dangerous reasoning usually goes: the user logged in, the token is valid, the signature checks out, the session exists. Therefore, the request can pass.

This reasoning opens a very dangerous security hole, because someone can access or change a resource that belongs to another person, another company, another business context or another tenant (each customer isolated inside a shared system).

Can an authenticated user access any order?
Can they download any invoice?
Can they look up any customer?
Can they reprocess any payment?
Can they transfer money from any account?
Can they change any setting?
Can they access data from any store, company, tenant or account?

Having a token answers only this question: who is this user?

However, that is not enough. The next question must be: what can this user do on this resource, in this context, right now?

This flaw shows up in simple scenarios like:

GET /orders/123
GET /invoices/456
POST /payments/789/refund
PATCH /users/10/role

The system checks whether the token is valid, but does not check whether that user can access that resource, that order, that invoice, that payment or that profile.

When doing GET /orders/123, the question cannot stop at "is the token valid?". It needs to chain into others. Does this order belong to the logged-in user? Can this user fetch this information? Does this order belong to the correct tenant? Does the state of this order allow this action? Is there any business rule limiting this access?

Solving this requires less sophistication than it seems. You do not need to implement a full ABAC engine (attribute-based access control) or replicate something like AWS IAM. In most cases, it is enough to validate whether the user owns the given resource or has explicit authorization to view or change it. A query that checks the link between user and resource already closes most of this gap.

This is how one of the most common flaws in systems is born, the authenticated user with the ability to access a resource they should not access. OWASP gives it a name. In the OWASP API Security Top 10, it holds the first position as Broken Object Level Authorization (BOLA), an evolution of what was for years called IDOR (Insecure Direct Object Reference). In the traditional OWASP Top 10, it belongs to the Broken Access Control category, which has led the list since the 2021 edition.

And an important detail: security through obscurity is not security.

It is no use believing that nobody will swap the ID in the URL, discover the endpoint, replay a call, inspect the traffic or assemble a manual request. If the backend accepts the operation just because the token exists, the system has confused identity with permission.

Authorization needs to consider action, resource and context.

It is not enough to ask "is the user logged in?".
You need to ask "can they execute this action on this resource?".

4. Blind trust in JWT claims

Chaining from that, the next flaw is observed when the team wants an easy way to already have some information at hand. The JWT (JSON Web Token) is a signed token that carries statements about the user, the so-called claims, in a format that any party can read. And it is common to see it carrying role, permissions, tenantId, userId, organizationId, scopes.

When the service grants access to sensitive or administrative functions just because a role or permission claim says it can, without revalidating that decision on the server, the OWASP API Security Top 10 classifies the problem as Broken Function Level Authorization (BFLA), fifth position on the list.

Carrying context in the token helps.

But it also creates a problem.

JWT becomes a very tempting place to carry too much context. And, in some systems, everything starts showing up there: national ID numbers, full name, email, phone, documents, profile data, organization information, permissions, flags and attributes that could be protected somewhere else.

This care also matters through a privacy and data protection lens, such as Brazil's LGPD or the GDPR. A JWT often travels in a header, may appear in logs, may be copied into debugging tools, may be inspected by different teams, may be stored on the client and may circulate through intermediate layers.

Even signed, the content of a JWT is normally not secret. The signature guarantees integrity. The payload remains readable to anyone who has the token in hand, simply by decoding the Base64.

And then someone may think: "so just encrypt the token".

It is not that simple. There is no free lunch, someone is always paying for it.

Encrypting the token's content can make sense in specific scenarios, but it increases operational complexity. Now you need to take care of encryption keys, rotation, compatibility between services, harder debugging, interoperability with libraries and providers, plus the risk of turning the token into a black box that is hard to govern. In many cases, encrypting becomes an attempt to compensate for an application design problem that put too much data in the token.

The healthier path is usually to reduce what goes inside it. Putting personal data in the token should not be the default.

The ideal is to carry only what is necessary for identification and access decisions, avoiding turning the token into a complete dossier of the user. The more sensitive data you put there, the bigger the impact when that token leaks, gets logged improperly or is handled carelessly by the client application.

Another point is that claims are a snapshot of the moment the token was issued. But permissions change, the user changes departments, a contract gets canceled, access gets revoked, a person leaves the company, a tenant gets deactivated, a role gets changed, a scope stops being valid.

Validating expiration helps, but it does not solve everything.

A token with a short expiration reduces the risk window. A well-handled refresh token improves session control. Revocation, introspection, permission versioning or rechecking on critical actions may be necessary depending on the domain. But merely putting exp in the JWT does not guarantee the decision is still valid throughout the token's entire lifetime.

Another basic and extremely important point: validating the signature is mandatory.

sequenceDiagram
    participant C as Client
    participant S as Service (Resource Server)
    participant IdP as Identity Provider

    C->>IdP: Login with credentials
    IdP-->>C: Signed JWT
    C->>S: Request with JWT in the header
    S->>IdP: Fetches public key (JWKS endpoint)
    IdP-->>S: Public keys
    S->>S: Validates signature, issuer and expiration
    S->>S: Decides authorization (action, resource, context)
    S-->>C: Response (or 401/403)

In a Spring Boot application, for example, it is common for the service to act as an OAuth2 Resource Server. It receives the JWT, identifies who issued the token, fetches the public key from the identity provider, usually through a JWKS endpoint, and uses that key to validate the signature. With that, the service can verify whether the token was issued by a trusted source and whether the content was not tampered with along the way.

But signature is not complete authorization.

It proves that the token is intact and came from whoever should have issued it. It still remains to prove that the user can execute that action on that specific resource.

This is the central point. JWT can help transport context, but it should not replace the application's authorization model.

Using claims is legitimate. The problem lies in treating them as the final answer for any access decision.

This becomes even more evident when the claim turns into overly generic authorization.

role = admin

Admin of what?
Of which organization?
Of which store?
Of which account?
With permission for which action?
In which environment?
With which limit?

For simple actions, well-defined claims may be enough. For critical actions, sensitive resources, multi-tenant environments and mutable permissions, the backend needs to validate context more carefully.

JWT should support the decision, never replace it.

The service still needs to ask: is this claim enough for this action, on this resource, in this context, right now?

What these four mistakes have in common

These four problems look different, but they share the same root, trust. Many flaws are born precisely from an excess of it.

The frontend is trusted to validate input.
The frontend is trusted to control permission.
The token is trusted to replace authorization.
The claim is trusted to replace context.

In every case, the application outsources a security decision to something that should be only part of the flow.

These practices matter, and their absence exposes an engineering gap. Security needs to appear before pentest, audit, pipeline, scanner or incident. It needs to be part of the basic repertoire of whoever develops software. Knowing how to validate input, model authorization, reduce exposure, protect sensitive data, think about flow abuse and question implicit trust should be part of the daily work of development.

This is where DevSecOps stops being just an acronym or a buzzword.

Just like DevOps, DevSecOps means bringing security close to the delivery flow, where decisions are made, instead of creating a role, a person or a department that receives the problem afterwards. It is also far from meaning dumping more responsibility on the developer without context, time and tooling. It is practice embedded into development, code review, testing, architecture, pipeline and operation, without turning into bureaucracy at the end.

Secure practice is less glamorous, but it is part of the work of a software engineering professional.

Validate in the backend.
Authorize in the backend.
Check action, resource and context.
Treat claims as support.
Do not let the interface be the security boundary.

This is the logic of shift left, the idea of moving verifications to the beginning of the flow, applied in practice. Anticipating security prevents insecure decisions from being discovered only when they are already expensive, widespread and hard to fix.

Security in development translates into small conscious decisions repeated every day, much more than into tools.

And one of the most important ones is simple. Beyond thinking about what to do, think about who does it. Do not trust what was only supposed to inform, facilitate or transport context.

The security decision needs to live where the rule is protected.