Technology

The Future of Technology Starts Here

AI · Web3 · Cloud · Cyber · next-gen dev

Programming

I Audited My Own Code: 10 Security Bugs I Found and Fixed

Description

Security vulnerabilities are not always hidden in complex systems or large enterprise applications. Sometimes, they exist in small projects that developers build every day. Hardcoded passwords, unsafe input handling, weak authentication, excessive permissions, and outdated dependencies can expose an application to serious security risks.

Introduction

As developers, we often focus on building features, improving performance, and delivering projects on time. However, security can easily become an afterthought. A small application may appear harmless during development, but a single vulnerability can expose sensitive user information, allow unauthorized access, or compromise an entire system.

To understand how these issues happen, I decided to audit a fictional Node.js application built with Express and a SQL database. The application allows users to register, log in, update their profiles, and access protected resources.

During the audit, I focused on five important areas: secret management, input validation, authentication, authorization, and dependency security. I also examined session handling, error messages, and security-related configuration.

The goal was not simply to find mistakes but to understand how they could be exploited and how to fix them properly.

Note: The code examples in this article are simplified illustrations for educational purposes. A production application may require additional protections based on its architecture and threat model.

Content

Bug 1: Hardcoded Secrets in Source Code
Original code:

const DB_PASSWORD = "admin123";
const JWT_SECRET = "mysecret";
The problem: Hardcoded credentials can leak through public repositories, shared files, or application logs. Anyone who obtains these values may gain unauthorized access to databases or forge authentication tokens.

The fix:

const DB_PASSWORD = process.env.DB_PASSWORD;
const JWT_SECRET = process.env.JWT_SECRET;

if (!DB_PASSWORD || !JWT_SECRET) {
throw new Error("Required security configuration is missing");
}
Sensitive values should be stored in environment variables or a dedicated secrets manager. Never commit real credentials to version control, and rotate any secrets that have already been exposed.

Bug 2: SQL Injection Through Unsafe Input
Original code:

const query =
`SELECT * FROM users WHERE email = '${req.body.email}'`;
The problem: An attacker could manipulate the email input to change the SQL query's meaning, potentially bypassing authentication or accessing unauthorized data.

The fix:

const result = await db.query(
"SELECT * FROM users WHERE email = $1",
[req.body.email]
);
Parameterized queries separate user input from SQL instructions. This is a fundamental defense against SQL injection. Input validation is also necessary, but it should not replace parameterized queries.

Bug 3: Weak Password Storage
Original code:

const passwordHash = password;
The problem: Storing passwords as plain text means anyone who obtains the database can immediately read users' passwords. Ordinary fast hashes, such as unsalted SHA-256, are also unsuitable for password storage.

The fix:

const argon2 = require("argon2");

const passwordHash = await argon2.hash(password);
Use a dedicated password-hashing algorithm such as Argon2id, with appropriate parameters. During login, verify the submitted password against the stored hash using the library's verification function. Never store or log plain-text passwords.

Bug 4: Missing Input Validation
Original code:

app.post("/register", async (req, res) => {
const user = await createUser(req.body);
res.json(user);
});
The problem: The application accepts arbitrary request data without checking whether required fields exist or contain valid values. This may lead to unexpected behavior, invalid records, or mass-assignment vulnerabilities.

The fix:

const { email, password } = req.body;

if (
typeof email !== "string" ||
email.length > 254 ||
typeof password !== "string" ||
password.length < 12 ||
password.length > 128
) {
return res.status(400).json({
error: "Invalid registration details"
});
}
Use a trusted validation library for more complete checks, including email format, password policy, and request size. Validate every field on the server, even when the frontend already performs validation.

Bug 5: Weak Authentication
Original code:

if (user.password === req.body.password) {
res.json({ message: "Login successful" });
}
The problem: This example compares plain-text passwords and does not establish a secure authenticated session. In a real application, such logic can expose credentials and leave protected resources without reliable identity verification.

The fix:

const valid = await argon2.verify(
user.passwordHash,
req.body.password
);

if (!valid) {
return res.status(401).json({
error: "Invalid email or password"
});
}
After verification, establish a secure session or issue a properly configured authentication token. Add rate limiting, multi-factor authentication where appropriate, and generic login errors to reduce account enumeration and brute-force risks.

Bug 6: Missing Authorization Checks
Original code:

app.get("/users/:id", authenticate, async (req, res) => {
const user = await getUserById(req.params.id);
res.json(user);
});
The problem: Authentication confirms who a user is, but it does not automatically grant permission to access every user's information. An attacker could change the ID in the URL and retrieve another user's data.

The fix:

app.get("/users/:id", authenticate, async (req, res) => {
if (req.params.id !== req.user.id) {
return res.sendStatus(403);
}

const user = await getUserById(req.user.id);
res.json(user);
});
In production, use consistent ID types and enforce ownership or role-based access policies on the server. Apply authorization checks to every sensitive operation, including reading, editing, and deleting resources.

Bug 7: Excessive User Permissions
Original code:

const user = await createUser({
...req.body
});
The problem: Copying the entire request body into a database operation may allow users to set protected fields, such as role: "admin", if the data model accepts them.

The fix:

const user = await createUser({
name: req.body.name,
email: req.body.email,
passwordHash: await argon2.hash(req.body.password)
});
Only accept explicitly permitted fields. Enforce least privilege across database accounts, API credentials, cloud permissions, and application roles. Ordinary users should never be able to grant themselves administrative access.

Bug 8: Insecure Session Configuration
Original code:

app.use(session({
secret: "session-secret",
cookie: { secure: false }
}));
The problem: A hardcoded session secret and cookies that are not restricted to HTTPS increase the risk of session compromise.

The fix:

app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 30 * 60 * 1000
}
}));
Use a cryptographically strong session secret and HTTPS in production. Configure a suitable production session store, regenerate session identifiers after login, and invalidate sessions during logout. Applications using cookie-based authentication should also assess their CSRF protection requirements.

Bug 9: Exposing Sensitive Error Messages
Original code:

app.use((err, req, res, next) => {
res.status(500).send(err.stack);
});
The problem: Stack traces may reveal internal file paths, database details, and implementation information that attackers can use to plan further attacks.

The fix:

app.use((err, req, res, next) => {
console.error("Application error:", err);

res.status(500).json({
error: "An unexpected error occurred"
});
});
Use structured server-side logging with appropriate access controls. Avoid recording passwords, access tokens, session identifiers, or other sensitive information. Detailed debugging information should remain in protected development and monitoring systems.

Bug 10: Vulnerable or Outdated Dependencies
Original code:

{
"dependencies": {
"express": "*"
}
}
The problem: Unrestricted dependency versions can make builds unpredictable and increase exposure to known vulnerabilities. Even correctly pinned packages can become vulnerable as new security advisories emerge.

The fix:

{
"dependencies": {
"express": "^5.1.0"
}
}
This version is an illustrative example, not a claim that it is the latest or appropriate version for every project. Select a supported release compatible with your application, commit the lockfile, and review dependency changes.

Run regular security checks:

npm audit
npm audit fix
Review the audit results before applying updates, run automated tests afterward, and use trusted dependency monitoring tools. Do not assume that every reported issue is exploitable in your application or that every automatic fix is risk-free.

Conclusion

Auditing my own code revealed an important lesson: application security depends on many small decisions, not just one powerful security tool. Hardcoded secrets, unsafe SQL queries, weak password handling, missing authorization checks, excessive permissions, and vulnerable dependencies can create serious weaknesses even in a relatively small application.

The good news is that these vulnerabilities can often be prevented through established security practices. Store secrets securely, validate untrusted input, use parameterized queries, hash passwords with suitable algorithms, enforce authorization, configure sessions safely, and keep dependencies maintained.

Security auditing should also be an ongoing process rather than a one-time activity. Review code changes, automate dependency checks, test authentication and authorization, and use tools such as static application security testing (SAST) and dynamic application security testing (DAST) where appropriate.

Most importantly, do not wait until an application is deployed to think about security. Build secure coding practices into your everyday development workflow.

My biggest takeaway: Writing code that works is only half the job. Writing code that protects its users is what makes it trustworthy.

Published: October 11, 2026
← Previous Article Ransomware Under the Hood: How File Encryption Attacks Work