Skip to main content

Command Palette

Search for a command to run...

Security

Updated
13 min readView as Markdown
N

Hello, I’m Nripesh

I am an aspiring programmer, continuously learning and building projects one step at a time.

This blog will serve as a record of my journey, where I document the coding challenges I encounter each week and the solutions I work towards. My intention is not only to track my own growth but also to provide clarity for others who may face similar difficulties.

What you can expect here:

Weekly summaries of the issues I struggled with

Solutions, explanations, and key insights I gained from them

References and links to helpful resources, documentation, or my GitHub repositories

Occasional notes on personal projects I am developing

I am still in the early stages of my learning path and have much to improve, but I approach each challenge with confidence and persistence. This space is both a personal archive and an attempt to contribute—however modestly—to the wider learning community.

If you are also navigating the world of programming, I welcome you to connect and share perspectives.

The single most important concept to focus on while designing a Backend or even Server security. Almost all cyberattacks stems from vulnerability of a system, or in simpler terms, the assumptions taken while creating a system.

Injection Attacks

Injection attacks remain one of the most critical and recurring security vulnerabilities in backend systems. At their core, these attacks exploit a fundamental mistake: treating untrusted user input as executable code rather than as plain data.


Understanding the Problem

In a typical backend flow, user input collected via frontend interfaces (e.g., form fields, APIs) is passed downstream to services like databases. If this input is directly concatenated into executable queries, it can alter the intended logic.

Consider a simple SQL query:

SELECT * FROM users WHERE email = '<user_input>'

If an attacker provides:

' OR '1'='1' --

The query logic is manipulated to always evaluate as true, potentially exposing all user records. More destructive payloads can go further, attempting operations like dropping entire tables.

The root issue is not the database—it is the unsafe construction of queries.


SQL Injection: Prevention Strategies

The industry-standard solution is parameterised queries (prepared statements).

  • The query structure is defined first.

  • User input is passed separately as bound parameters.

  • The database treats input strictly as data, not executable SQL.

This ensures that even malicious inputs are interpreted as harmless strings.

In addition, modern backend systems often rely on ORMs (Object-Relational Mappers) or query builders, which abstract raw SQL and enforce safe query construction by default.

Input validation can serve as a secondary layer:

  • Enforcing formats (e.g., valid email structure)

  • Restricting input length

  • Ensuring type correctness

However, validation alone is insufficient—it reduces risk but does not eliminate it.


Command Injection: A More Severe Variant

While SQL injection targets databases, command injection targets the operating system.

A vulnerable pattern looks like:

system("ping " + user_input);

If the input includes shell operators, such as:

8.8.8.8; rm -rf /

The system may execute unintended and potentially destructive commands.


Preventing Command Injection

Mitigation strategies here are stricter:

  • Avoid shell execution whenever possible Use native libraries or APIs instead of invoking system commands.

  • Use safe execution mechanisms If execution is unavoidable, pass arguments as structured parameters (not concatenated strings), avoiding shell parsing.

  • Apply strict input whitelisting Only allow inputs that conform to expected patterns (e.g., valid IP addresses or domain names).


General Security Principle

A reliable rule of thumb across all injection classes is:

All external input must be treated strictly as data and must never be directly interpreted as code.

Or more succinctly:

Always separate code from data at every system boundary.


Authentication and Access Control

Authentication is a foundational pillar of backend security. It governs how systems verify identity, manage access, and protect sensitive operations. While it may appear straightforward, production-grade authentication involves careful design choices, trade-offs, and layered safeguards.


Start with an Auth Provider

For most applications, especially in their early stages, integrating a trusted authentication provider (such as OAuth-based solutions like Google Sign-In) is the most practical and secure approach. These systems are battle-tested and designed with security as a priority, significantly reducing both implementation effort and risk.

They also handle several non-trivial concerns out of the box:

  • Account linking (e.g., email/password + OAuth for the same user)

  • Token lifecycle management

  • Built-in support for features like MFA

A common real-world case is when a user signs up using email/password and later logs in using OAuth with the same email. A well-designed system should unify these identities, which external providers manage seamlessly.

Building secure authentication systems is a specialised problem—mature systems dedicate entire teams to it, making custom implementations hard to justify early on.


Password Storage: From Hashing to Modern Standards

When implementing native authentication, password handling must follow strict security practices.

At a minimum, passwords must never be stored in plaintext. Instead, they are passed through a hashing function:

password → hash(password)

This ensures that even if the database is compromised, raw passwords are not exposed.

However, hashing alone is insufficient. To prevent precomputed attacks (rainbow tables), salting is introduced:

hash(password + random_salt)
  • Each user gets a unique salt

  • Identical passwords produce different hashes

  • Pre-computation attacks become ineffective

With modern hardware (especially GPUs), brute-force attacks became faster, making traditional fast hashing algorithms obsolete. This led to the adoption of slow hashing functions such as:

  • bcrypt

  • Argon2id

These algorithms are intentionally computationally expensive and allow configurable cost factors, significantly increasing the difficulty of brute-force attacks.


Session-Based Authentication (Stateful)

In stateful systems, authentication is managed via server-side sessions. Once a user logs in, the server creates a session and stores it, returning a session ID to the client via cookies. Each subsequent request includes this identifier for validation.

The security of this model depends heavily on proper cookie configuration:

  • HttpOnly → prevents access via JavaScript (mitigates XSS)

  • Secure → ensures cookies are only sent over HTTPS

  • SameSite:

    • Strict → blocks all cross-site requests

    • Lax → allows limited cross-site usage

    • None → allows cross-site (requires Secure)


Token-Based Authentication (Stateless)

Stateless authentication replaces server-side sessions with tokens, typically JWTs. A JWT consists of:

  • Header → metadata about the token

  • Payload → user claims (e.g., ID, roles, expiry)

  • Signature → ensures integrity

This approach removes the need for server-side storage and scales well in distributed systems. However, it introduces key trade-offs:

  • Tokens cannot be easily revoked once issued

  • Validity persists until expiration

Common mitigation techniques include:

  • Short-lived access tokens

  • Refresh tokens

  • Token blacklisting (partial workaround)

A critical detail often overlooked is that JWT payloads are not encrypted—they are only base64 encoded. This means:

  • Anyone can decode and inspect them

  • Sensitive data must never be included


Token Storage and Trade-offs

Where tokens are stored significantly impacts security:

  • Local Storage

    • Simple to implement

    • Vulnerable to XSS attacks

  • HttpOnly Cookies

    • Not accessible via JavaScript

    • More secure in practice

At this point, JWTs stored in cookies start resembling traditional sessions. The distinction becomes more about architecture than behaviour:

  • Sessions → easier revocation, stronger control

  • JWTs → better scalability, more complexity

In most cases, stateful authentication is the safer default unless statelessness is a clear requirement.


Rate Limiting (Protecting Against Abuse and DDoS)

Authentication endpoints are frequent targets for brute-force attacks, credential stuffing, and denial-of-service attempts. Rate limiting acts as a critical safeguard.

Common approaches include:

  • Per-IP limiting

    • Restricts requests from a single IP

    • Can affect users behind shared networks

  • Per-account limiting

    • Limits attempts per user account

    • Can be bypassed with multiple accounts

  • Global limiting

    • Caps total incoming requests

    • Protects infrastructure under heavy load

In practice, effective systems combine these strategies to build a layered defense rather than relying on a single mechanism.


Authorisation Security

While authentication verifies who a user is, authorisation determines what they are allowed to do.

Broken Object-Level Authorisation (BOLA)

Broken Object-Level Authorisation occurs when a system fails to verify whether a user has access to a specific resource.

A typical vulnerable pattern:

  • The API validates that the user is authenticated

  • Fetches a resource based on an ID (e.g., /invoice/123)

  • Returns it without verifying ownership

Root cause: Authorisation checks are either missing or performed only at the API layer without validating against actual data ownership.


Correct Approach

Authorisation must be enforced at the data access level, not just at the API boundary.

Instead of:

  • Fetch resource → then check ownership → return 403 Forbidden

Use a single constrained query:

SELECT * FROM invoices WHERE id = 123 AND user_id = 9

This ensures:

  • If the resource does not belong to the user → no data is returned

  • The system responds with 404 Not Found instead of 403 Forbidden

This avoids leaking information about the existence of resources owned by other users.


Broken Function-Level Authorisation

This occurs when users can access functionality they are not permitted to use.

A common example:

  • Admin endpoints (e.g., fetching all users) are exposed

  • No proper role check is enforced

This often stems from security through obscurity, where developers assume that hiding endpoints (e.g., /admin-panel) is sufficient.


Correct Approach

  • Enforce role-based checks at middleware or service layer

  • Every sensitive function must validate:

    • User role (e.g., admin)

    • Permissions explicitly

Endpoint visibility is not a security control—authorisation checks are.


Indirect Object Reference & Predictable IDs

When systems expose sequential or predictable identifiers:

/user/101  
/user/102  

Attackers can easily enumerate and access other users’ data.

This is known as Insecure Direct Object Reference (IDOR), a form of broken access control.


Mitigation

  • Use non-guessable identifiers (e.g., UUIDs)

  • Combine with proper authorisation checks (UUID alone is not sufficient)


Types of Authorisation Attacks

Authorisation flaws generally manifest in two forms:

  • Horizontal Privilege Escalation A user accesses another user’s data at the same privilege level (e.g., User A accessing User B’s invoice)

  • Vertical Privilege Escalation A user gains access to higher-level functionality (e.g., a normal user accessing admin endpoints)

Both stem from insufficient or improperly enforced authorisation logic.


Designing Secure Authorisation Systems

Robust authorisation is not achieved through isolated checks—it requires systemic design principles:

1. Centralise Authorisation Logic

  • Avoid scattering checks across multiple services or endpoints

  • Use middleware or dedicated authorisation layers

2. Default Deny

  • If access is not explicitly allowed → deny it

  • Never assume access unless verified

3. Enforce Authorisation at Data Layer

  • Combine access checks with data queries

  • Avoid fetching data before validating ownership

4. Test Authorisation Explicitly

  • Write dedicated test cases for access control

  • Even small changes can unintentionally introduce vulnerabilities

5. Maintain Audit Logs

  • Log access attempts, especially failures

  • Helps detect probing, enumeration, and attack patterns early


Cross-Site Scripting (XSS) and Common Web Vulnerabilities

Cross-Site Scripting (XSS) is one of the most prevalent client-side vulnerabilities, but its root cause lies in backend assumptions. It occurs when untrusted user input is treated as executable code in the browser, allowing attackers to inject scripts that run in the context of another user’s session.


Why XSS is Dangerous

XSS is not just about injecting JavaScript—it enables attackers to operate as the victim user within the application.

Typical impacts include:

  • Session hijacking (stealing cookies or tokens)

  • Phishing attacks (injecting fake login forms or UI elements)

  • Content manipulation (altering what the user sees)

  • Performing actions on behalf of the user (if the app relies on cookies for auth)

Because the script executes in the user’s browser, it inherits the same privileges as the legitimate application.


How XSS Happens

At its core, XSS arises from a flawed assumption:

User-provided content will always be treated as data, not as executable instructions.

This assumption breaks when input is rendered directly into HTML without proper handling.


Stored XSS (Persistent)

Stored XSS is particularly dangerous because the malicious payload is saved on the server and served to multiple users.

A common scenario involves rendering user-generated content such as markdown.

If a developer builds a custom renderer without proper safeguards, an attacker can inject:

<script>alert('XSS')</script>

If this is stored and later rendered as HTML, the browser executes it as code.


Prevention: Sanitisation and Safe Rendering

The primary defence against XSS is sanitisation—ensuring that user input cannot be interpreted as executable code.

Key practices:

  • Use trusted libraries for rendering (e.g., markdown parsers that sanitise by default)

  • Strip or escape dangerous tags (<script>, event handlers like onClick)

  • Treat all user input as untrusted by default

Never attempt to manually parse or sanitise complex formats like HTML or markdown—this is error-prone.


Content Security Policy (CSP)

CSP acts as an additional defence layer by restricting what the browser is allowed to execute.

It allows you to define rules such as:

  • Only allow scripts from trusted domains

  • Block inline scripts

  • Restrict image or resource loading sources

However:

CSP is a last line of defence, not a primary solution. Proper input sanitisation must always come first.


General Rule for XSS Prevention

All forms of XSS share the same root cause:

Assuming user input cannot be interpreted as code.

The correct mindset is:

Every piece of user-generated content must be validated and sanitised before rendering.


Other Common Web Vulnerabilities

1. Misconfiguration

Security misconfigurations are often overlooked but highly impactful.

Examples include:

  • Hardcoded secrets in codebases

  • Exposed environment variables


2. Debug Mode in Production

Running applications in debug mode in production environments exposes excessive internal details such as:

  • Stack traces

  • File paths

  • System architecture hints

This information can significantly aid attackers in crafting targeted exploits.

In production:

  • Disable debug mode

  • Limit logs to necessary levels (e.g., info, error)


Security Headers

HTTP security headers provide additional protection at the browser level.

Common examples:

  • X-Frame-Options → prevents clickjacking (embedding your site in iframes)

  • Content-Security-Policy → controls resource loading and script execution

  • Strict-Transport-Security (HSTS) → enforces HTTPS usage

These headers strengthen the security posture but should complement—not replace—secure coding practices.


Conclusion

Web security is not static—it continuously evolves alongside the systems it aims to protect. As applications grow more complex, so do the attack vectors targeting them. There is no single mechanism that guarantees security; instead, it is achieved through consistent, layered defences and disciplined engineering practices.

The most important mindset shift is this:

Never assume input is safe, and never assume a boundary is secure unless it is explicitly enforced.

In a rapidly evolving ecosystem, security is less about reacting to known vulnerabilities and more about designing systems that minimise assumptions and reduce the possibility of exploitation by default.