Security
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 requestsLax→ allows limited cross-site usageNone→ 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 Foundinstead of403 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 likeonClick)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.