Web Application Security Basics - Protecting Websites and APIs from Common Risks
Security Starts at the Request
A web application looks simple to the user. They open a page, enter details, click a button, and get a result. But behind that simple action, the browser sends a request, the server processes it, the database returns data, and the response goes back to the browser.
This request flow is where most web security problems begin.
A beginner developer usually tests the application in the normal way. They enter the correct email, correct password, valid form data, and expected file type. If everything works, the feature feels complete.
But real users and attackers do not always behave normally. Someone may change values in the browser developer tools. Someone may call the API directly without using the frontend. Someone may enter script code into a comment box. Someone may try many passwords again and again. Someone may change an ID in the URL and try to open another user’s data.
Web application security is about preparing the application for these unsafe situations.
Basic Web Application Flow
Browser
|
| HTTPS request
v
Frontend page or frontend app
|
| API call
v
Backend server
|
| Validate + Authenticate + Authorize
v
Database or external service
|
v
Response back to user
Each layer has a security responsibility.
The frontend helps the user interact with the system. The backend protects business logic. The database stores important data. HTTPS protects data in transit. Logs and monitoring help developers find problems after deployment.
A secure web application does not depend on one single protection method. It uses many small checks across the full flow.
Link to:Authentication Password Security
Frontend Checks Are Not Enough
Frontend validation is useful, but it is not real protection by itself.
For example, a signup page may show an error if the password is less than eight characters. That is good for user experience. But a user can bypass the browser and send an API request directly.
A hidden admin button is also not security. A normal user may not see the admin button in the UI, but they can still try to call the admin API directly.
The backend must protect the system.
Frontend = guides the user
Backend = enforces the rules
This is one of the first security lessons every web developer should understand.
Server-Side Input Validation
Input validation means checking whether incoming data is acceptable before using it.
Web apps receive input from many places:
Login forms
Signup forms
Search boxes
Comment sections
URL parameters
API request body
Cookies
Headers
File uploads
Query strings
A normal user may enter expected values. But the application should also handle unexpected values safely.
For example, a product ID should be a valid ID. A page number should be a number. An email should look like an email. A file upload should match allowed types and size limits.
Simple Express Validation Example
function validateSignup(req, res, next) {
const { name, email, password } = req.body;
if (!name || typeof name !== "string" || name.length > 50) {
return res.status(400).json({
message: "Enter a valid name."
});
}
if (!email || typeof email !== "string" || !email.includes("@")) {
return res.status(400).json({
message: "Enter a valid email address."
});
}
if (!password || password.length < 8) {
return res.status(400).json({
message: "Password must be at least 8 characters."
});
}
next();
}
This example is simple, but the idea is important. The backend checks the input before creating the user.
For larger projects, developers usually use validation libraries such as Zod, Joi, Yup, express-validator, or framework-specific validation tools.
Link to:JWT Security
Authentication
Authentication confirms who the user is.
The most common authentication method is login with email and password. Some applications use OTP, OAuth, social login, magic links, or single sign-on.
Authentication answers this:
Who is this user?
A login system should not only check credentials. It should also protect passwords, limit repeated attempts, create secure sessions or tokens, and avoid revealing too much information in error messages.
A risky login message:
This email exists, but the password is wrong.
A safer login message:
Invalid email or password.
The safer message does not reveal whether the email exists in the system.
Password Hashing
Passwords should never be stored as plain text.
If an attacker gets access to a database and passwords are stored directly, all user accounts become exposed. A safer application stores password hashes.
A hash is a one-way value created from the password. During login, the entered password is compared with the stored hash.
Use trusted password hashing algorithms such as bcrypt, Argon2, or PBKDF2. Do not create your own password hashing logic.
Installing bcrypt
npm install bcrypt
Password Hashing Example
const bcrypt = require("bcrypt");
async function hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password, storedHash) {
return await bcrypt.compare(password, storedHash);
}
A beginner mistake is using fast hash functions like MD5 or SHA-1 for password storage. These are not suitable for passwords because attackers can test many guesses quickly.
Authorization
Authorization checks what an authenticated user is allowed to access.
Authentication says who the user is. Authorization says what the user can do.
Example:
A user logs in successfully. That is authentication.
The same user tries to open another user’s order. The backend should block it. That is authorization.
A web application should check authorization on the backend for every sensitive action.
Link to: Cybersecurity basics
Ownership Check Example
Imagine an order API. A user should only see their own orders.
app.get("/api/orders/:orderId", async (req, res) => {
const orderId = req.params.orderId;
const userId = req.user.id;
const order = await Order.findOne({
_id: orderId,
userId: userId
});
if (!order) {
return res.status(404).json({
message: "Order not found."
});
}
res.json(order);
});
This API checks both the order ID and the logged-in user ID. It does not return an order just because the order exists.
This prevents users from changing the URL and accessing someone else’s data.
Role-Based Access Control
Role-Based Access Control gives permissions based on roles.
Common roles:
User
Admin
Editor
Seller
Manager
Support
A normal user should not access admin actions. A seller should manage only their own products. An editor may create or update articles but should not delete users.
Simple Role Middleware
function requireRole(role) {
return function (req, res, next) {
if (!req.user || req.user.role !== role) {
return res.status(403).json({
message: "Access denied."
});
}
next();
};
}
app.delete("/api/admin/users/:id", requireRole("admin"), deleteUser);
This protects the admin delete route from non-admin users.
In real applications, permissions may be more detailed than a single role. But role-based access is a good starting point for beginner and intermediate projects.
Secure Cookies
Many web applications use cookies to remember login sessions.
A secure cookie should use proper settings.
res.cookie("sessionId", sessionId, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 1000 * 60 * 60
});
Important cookie options:
HttpOnly → JavaScript cannot read the cookie
Secure → cookie is sent only over HTTPS
SameSite → helps reduce CSRF risk
Cookies are powerful, but they should be handled carefully. A stolen session cookie can act like a stolen login key.
Token Security
Some applications use tokens such as JWT for authentication.
Tokens should be protected like passwords because they can give access to user accounts.
Unsafe token practices:
Storing tokens in public code
Sending tokens in URL parameters
Logging tokens in server logs
Keeping tokens valid forever
Using weak signing secrets
Safer habits:
Use short token expiry
Protect refresh tokens carefully
Use strong secrets
Do not print tokens in logs
Verify token signature on backend
JWT is useful, but it is not automatically secure. The implementation matters.
Link to:JWT Security
SQL Injection Protection
SQL injection happens when user input changes the meaning of a database query.
Unsafe query:
const query = "SELECT * FROM users WHERE email = '" + email + "'";
This is dangerous because the input is directly joined into the SQL string.
Safer parameterized query:
const query = "SELECT * FROM users WHERE email = ?";
db.query(query, [email]);
Parameterized queries separate user input from SQL logic.
ORMs can help, but developers should still be careful with raw queries. A project can use an ORM and still have SQL injection if raw unsafe queries are written.
Cross-Site Scripting Protection
Cross-Site Scripting, also called XSS, happens when harmful script code runs inside a user’s browser through your website.
This usually happens when user-provided content is displayed as HTML without safe handling.
Risky example:
document.getElementById("comment").innerHTML = userComment;
Safer example:
document.getElementById("comment").textContent = userComment;
textContent displays the input as text instead of treating it as HTML.
XSS can happen in comment sections, profile bios, rich text editors, dashboards, and any area where user content is displayed.
Modern frontend frameworks usually escape values by default, but dangerous raw HTML features can still create risk.
Cross-Site Request Forgery Protection
Cross-Site Request Forgery, or CSRF, tricks a logged-in user’s browser into sending an unwanted request.
This is more common when authentication depends on cookies.
Sensitive actions need protection:
Change password
Update email
Delete account
Transfer money
Change admin settings
Submit payment
Protection methods include CSRF tokens, SameSite cookies, checking request origin, and confirmation steps for sensitive actions.
The backend should not assume every request from a logged-in browser was intentionally made by the user.
Link to: Cybersecurity basics
Security Headers
Security headers help browsers protect users from certain attacks.
Important security headers include:
Content-Security-Policy
Strict-Transport-Security
X-Frame-Options
X-Content-Type-Options
Referrer-Policy
Permissions-Policy
In Express.js, Helmet can add many useful security headers.
Installing Helmet
npm install helmet
Using Helmet
const helmet = require("helmet");
app.use(helmet());
Helmet is not a complete security system, but it is a strong basic step for Node.js applications.
HTTPS in Production
HTTPS encrypts communication between the browser and the server.
Without HTTPS, login data, cookies, tokens, and private form values may be exposed on the network.
Production websites should use HTTPS. HTTP should redirect to HTTPS.
Check headers using:
curl -I https://example.com
A common mistake is enabling HTTPS for the main page but loading scripts, images, or API calls through HTTP. This creates mixed content issues.
A secure production app should avoid mixed content and use HTTPS everywhere.
File Upload Security
File upload features need careful handling.
Users may upload profile images, resumes, product photos, documents, or attachments. But attackers may upload harmful files, oversized files, renamed files, or unsupported formats.
A safe file upload flow should check:
Allowed file type
File size limit
File extension
Storage location
File name safety
Access permissions
Private vs public files
Basic Upload Validation Logic
const allowedTypes = ["image/png", "image/jpeg"];
const maxSize = 2 * 1024 * 1024;
function validateFile(file) {
if (!allowedTypes.includes(file.mimetype)) {
throw new Error("Unsupported file type");
}
if (file.size > maxSize) {
throw new Error("File is too large");
}
}
Do not execute uploaded files as code. Do not store private uploads inside public folders. Rename files before saving them.
For large production systems, use cloud storage, signed URLs, private buckets, and file scanning when required.
Rate Limiting
Rate limiting controls how many requests a user or IP address can send in a period of time.
Useful places for rate limiting:
Login
Signup
OTP request
Password reset
Contact form
Search API
Public API
Payment callback
Installing Rate Limit Package
npm install express-rate-limit
Login Rate Limit Example
const rateLimit = require("express-rate-limit");
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: {
message: "Too many login attempts. Please try again later."
}
});
app.post("/login", loginLimiter, loginController);
Rate limiting helps reduce brute-force attempts, spam, and basic abuse. It also protects server resources.
Safe Error Handling
Detailed errors are useful for developers but unsafe for users.
Bad production response:
{
"error": "MongoServerError at /app/src/database/connection.js"
}
Safer response:
{
"message": "Something went wrong. Please try again."
}
The server can still log details privately.
try {
await createOrder(req.body);
res.json({ message: "Order created successfully." });
} catch (error) {
console.error("Create order failed:", error.message);
res.status(500).json({
message: "Unable to create order at the moment."
});
}
This keeps the user response clean while still helping developers debug the issue.
Secrets Management
Secrets include private values used by the application.
Examples:
Database password
JWT secret
Cloud access key
Payment gateway secret
Email service password
Third-party API key
Bad practice:
const dbPassword = "mySecretPassword123";
Better practice:
const dbPassword = process.env.DB_PASSWORD;
Create a local environment file:
touch .env
Example .env values:
DB_PASSWORD=local_database_password
JWT_SECRET=local_jwt_secret
Add .env to .gitignore:
.env
node_modules/
dist/
build/
Never upload real secrets to public Git repositories. If a secret is accidentally exposed, rotate it immediately.
Dependency Security
Modern applications depend on external packages. These packages can have vulnerabilities or outdated code.
For Node.js projects:
npm audit
Update packages carefully:
npm update
Do not blindly update production dependencies without testing. A security update can sometimes introduce breaking changes.
A good workflow is:
Check vulnerability
Read package update notes
Update in development
Run tests
Deploy through normal pipeline
Monitor errors
Dependency security is part of regular maintenance.
Practical Secure Web Architecture
Browser
|
| HTTPS
v
CDN or reverse proxy
|
| TLS + security headers
v
Frontend application
|
| API request
v
Backend API
|
| validation
| authentication
| authorization
| rate limiting
v
Database
|
| limited access
| backups
v
Logs and monitoring
This architecture shows security as a layered system.
The browser communicates through HTTPS. The reverse proxy can handle TLS and headers. The backend checks rules. The database stays protected. Logs and monitoring help detect problems.
A safe web app does not depend only on frontend restrictions.
Production Security Checklist
Before deploying a web application, check these points:
HTTPS enabled
Debug mode disabled
Environment variables configured
Secrets not committed to Git
Database not publicly exposed
Passwords hashed
Input validation added
Authentication working
Authorization checked
Admin routes protected
Rate limiting added
Security headers enabled
File uploads restricted
Safe error messages used
Dependencies checked
Logs do not expose tokens or passwords
Backups configured
This checklist does not cover every advanced security topic, but it prevents many common beginner and intermediate mistakes.
Performance and Security Balance
Security should protect the system without making the application unusable.
Password hashing should be strong, but not so slow that normal login becomes painful. Rate limiting should block abuse, but should not block real users too quickly. Validation should be strict enough to protect the system, but clear enough to help users fix mistakes.
Security and performance should work together.
A production app should be safe, but it should also remain responsive.
Scalability and Security
Security design changes when an application grows.
In a single-server app, local sessions and local logs may be simple. In a multi-server app, sessions may need shared storage or token-based design. Logs should be centralized. Rate limits should work across all servers. File uploads may need cloud storage. Secrets should be managed through hosting or secret management tools.
Security should scale with the application.
A feature that works for 50 users may need stronger controls when the application reaches thousands of users.
Debugging Security Issues
Security bugs often hide behind normal-looking features.
Useful debugging steps:
Test the API directly without frontend
Try invalid input
Try accessing another user's data
Check status codes
Review backend logs
Inspect cookies
Check token expiry
Check security headers
Verify environment variables
Run dependency audit
Check database permissions
Useful commands:
curl -I https://example.com
curl -X POST https://example.com/api/login
npm audit
These simple checks help developers understand how the application behaves outside the normal browser flow.
Common Beginner Mistakes
Many web security issues come from repeated beginner mistakes.
Developers hide admin buttons but forget to protect admin APIs. They trust frontend validation only. They store passwords without hashing. They write SQL queries using raw input. They show full server errors to users. They upload .env files to GitHub. They allow unlimited login attempts. They accept every file upload. They forget ownership checks. They deploy with debug mode enabled.
These mistakes are common because beginners focus on visible features first.
A safer habit is to add security checks while building each feature.
Interview-Relevant Security Points
Interviewers often ask web security questions to check practical understanding.
Strong answers usually connect security terms to real development.
Authentication confirms identity.
Authorization checks permission.
Parameterized queries help prevent SQL injection.
Escaping output helps reduce XSS.
HttpOnly cookies protect cookies from JavaScript access.
HTTPS protects data in transit.
Rate limiting reduces brute-force attacks.
Secrets should be stored outside source code.
Backend validation is required even if frontend validation exists.
A good answer does not need to be complicated. It should show that the developer understands how security applies in real applications.
Daily Security Habits for Developers
A secure web application is built through repeated small habits.
When adding a form, validate input on the backend. When storing passwords, hash them. When returning user data, check ownership. When building admin features, enforce role checks. When displaying user content, avoid unsafe HTML. When deploying, use HTTPS. When handling errors, keep technical details private. When using packages, check for vulnerabilities. When storing secrets, keep them out of source code.
Security is not only a final review step. It is part of everyday development.
Link to:Authentication Password Security
Link to:JWT Security
Link to: Cybersecurity basics


Post a Comment