Coding with Caution: A Developer’s Guide to Secure Software Development

Sandalikaariyarathna
SLIIT Women In FOSS Community
12 min readApr 18, 2024

Introduction

When we build software, we’re not just crafting functionality and features; we’re also embedding layers of security that protect users from potential digital harm. Just like the foundation of a house, the security measures we integrate into our applications are critical to the overall integrity of the digital structures we create. Secure software development isn’t an afterthought or a box to check post-production; it’s a fundamental aspect of the development process.

The digital landscape is fraught with risks — cyber threats lurk in the shadows, ready to exploit any weakness. These vulnerabilities, if left unchecked, can lead to disastrous outcomes, such as data breaches, financial loss, and erosion of user trust. In extreme cases, they can even threaten personal safety.

As we embark on this journey through the intricacies of secure coding, remember: the code you write today could be the barrier that stands between a user and a cyber-attack tomorrow. Let’s code not just with innovation in mind, but with vigilance.

In the following sections, we will dissect some of the most common software vulnerabilities. We’ll understand their origins, explore their dangers, and most importantly, learn how to fortify our code against them. Each vulnerability will be accompanied by tangible JavaScript code examples — flawed snippets that we’ll then refactor, step by step, into more secure versions. By the end, you’ll not only recognize these vulnerabilities but also possess the knowledge to prevent them.

Understanding Vulnerabilities in Software

In the digital realm, the term ‘vulnerability’ signifies a weak spot in our software systems. Just as a crack in a dam can lead to a devastating flood, a vulnerability in software can open the door to cyberattacks that could compromise user data, disrupt services, and tarnish a company’s reputation.

What Exactly Are Software Vulnerabilities?

Software vulnerabilities are essentially flaws or weaknesses in a system that can be exploited by attackers to gain unauthorized access or cause harm. These can be as simple as a forgotten line of code or as complex as a deep-rooted logic error. They emerge from a variety of sources — some might be the result of oversight during coding, while others might arise from outdated libraries or frameworks.

Common Types of Vulnerabilities:

· SQL Injection (SQLi): This occurs when an attacker can “inject” malicious SQL commands into a database query, manipulating the database to reveal information, modify content, or even issue destructive commands.

· Cross-site scripting (XSS): XSS attacks allow an attacker to execute malicious scripts in another user’s browser, often resulting in the theft of cookies, tokens, or other sensitive information.

· Cross-Site Request Forgery (CSRF): In CSRF, an attacker tricks a user into performing actions they didn’t intend to, like submitting a form or changing their email address, by leveraging the user’s own authentication credentials.

· Insecure Direct Object References (IDOR): IDOR vulnerabilities occur when an application provides direct access to objects based on user input. This can lead to unauthorized access if proper security checks are not in place.

· Security Misconfiguration: This broad category includes unsecured storage of credentials, outdated software, unnecessary services running on the server, and default configurations that are not secured properly.

· Sensitive Data Exposure: Poorly protected data can lead to exposure during its storage or transfer over a network. Without proper encryption, attackers can easily intercept sensitive information.

· Use of Components with Known Vulnerabilities: Utilizing libraries and frameworks that have known weaknesses without patching them can make software susceptible to attacks that exploit these known vulnerabilities.

· Broken Authentication: When authentication measures are poorly implemented, attackers can compromise passwords, keys, or session tokens to assume users’ identities.

Each of these vulnerabilities requires a different approach when it comes to securing software against them. The goal of understanding these weaknesses is not to instill fear but to empower developers with the knowledge to build more secure systems. In the following sections, we will take a closer look at some of these vulnerabilities. We’ll explore how they might be introduced into our code, examine the potential consequences, and, importantly, learn how we can mitigate the risks through secure coding practices with concrete JavaScript examples.

1. SQL Injection (SQLi)

What is SQL Injection?

SQL Injection (SQLi) is a type of attack where the attacker manipulates a standard SQL query to execute malicious commands on the underlying database. This can happen when user input is directly included in queries without proper validation or sanitation. Attackers can use this vulnerability to view, modify, or delete data, even gaining administrative rights to a database.

Why Does It Occur?

SQLi occurs primarily due to the software not properly separating data (like user input) from SQL code. When user input is directly concatenated into a query, it can be crafted to alter the query’s structure and function. If the application does not rigorously validate this input, it becomes an open door for attackers.

Vulnerable JavaScript Code Example:

let username = req.body.username; // User input from a form field
let password = req.body.password; // User input from a form field
let query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
db.execute(query, (err, result) => {
// Process results
});

In the above code, user input (username and password) is directly placed into the SQL query. An attacker could input malicious SQL code into either field to manipulate the query.

Secured JavaScript Code Using Parameterized Queries:

Parameterized queries ensure that the input is treated strictly as data, not executable code. This prevents attackers from altering the SQL query structure.

let username = req.body.username; // User input from a form field
let password = req.body.password; // User input from a form field

let query = `SELECT * FROM users WHERE username = ? AND password = ?`;

db.execute(query, [username, password], (err, result) => {
// Process results
});

In the secured code example, ? placeholders are used in the SQL query, and the actual input values are passed as an array to the execute method. This ensures that the inputs are handled safely and cannot alter the intent of the SQL query, effectively mitigating the risk of SQL injection.

By using parameterized queries, developers can protect their applications from SQL injection attacks, preserving the integrity and security of their databases.

2. Cross-Site Scripting (XSS)

What is Cross-Site Scripting (XSS)?

Cross-site scripting, commonly known as XSS, is a type of security vulnerability typically found in web applications. XSS enables attackers to inject malicious scripts into content that other users see and interact with. These scripts execute in the victim’s browser and can steal cookies, session tokens, or other sensitive information that leads to identity theft, account tampering, and other malicious activities.

Why Does XSS Occur?

XSS vulnerabilities occur when an application includes untrusted data in a web page without properly validating or escaping it. This allows attackers to inject executable JavaScript code into the web page viewed by other users.

Vulnerable JavaScript Code Example:

Here’s how a simple piece of JavaScript code can become vulnerable to XSS:

// Imagine a scenario where user input is directly written into the webpage
let userInput = document.getElementById('userComment').value;
document.getElementById('commentsSection').innerHTML = userInput;

In this example, if userInput includes malicious script tags, they will be executed by the browser when rendering commentsSection.

Secured JavaScript Code to Prevent XSS:

To prevent XSS, you must sanitize user input, ensuring that it does not get executed as code. One way to do this is by encoding the user input as text, not HTML.

// Safely displaying user input by treating it as text, not HTML
let userInput = document.getElementById('userComment').value;
let textNode = document.createTextNode(userInput); // Creates a text node from the user input
document.getElementById('commentsSection').appendChild(textNode);

In the secured code, document.createTextNode(userInput) creates a text node, not an HTML element. This means any script tags or HTML tags in userInput will be treated as plain text and will not be executed by the browser. This effectively neutralizes the script and prevents XSS attacks.

By understanding and mitigating XSS vulnerabilities, developers can protect their web applications from a wide range of malicious activities, safeguarding both the application’s integrity and the users’ data.

3. Cross-Site Request Forgery (CSRF)

Understanding CSRF Attacks

Cross-Site Request Forgery (CSRF) is a web security vulnerability that tricks a user into executing unwanted actions on a web application where they are currently authenticated. An attacker can exploit this by sending a malicious request (e.g., via email or a malicious website) that can alter user data or perform actions on behalf of the user without their consent.

Why Does CSRF Occur?

CSRF attacks are possible when a web application doesn’t verify whether a request came genuinely from the user or was maliciously induced from another site. This can happen if the application only relies on cookies for authentication because browsers automatically include cookies with every request to a particular domain.

Vulnerable JavaScript Code Example:

Consider a web form that changes a user’s email address. A simple, vulnerable JavaScript form submission might look like this:

// JavaScript code for submitting a form - vulnerable to CSRF
document.getElementById('emailForm').addEventListener('submit', function(e) {
e.preventDefault();

let newEmail = document.getElementById('newEmail').value;
fetch('/change-email', {
method: 'POST',
body: JSON.stringify({ email: newEmail }),
credentials: 'include' // Automatically sends cookies
});
});

In this example, if an attacker can trick the user’s browser into submitting this form (perhaps from another site), the user’s email can be changed without their knowledge.

Secured JavaScript Code Using Anti-CSRF Tokens:

To protect against CSRF, you can use anti-CSRF tokens. These tokens are unique to each user session and ensure that the request is being sent intentionally by the authenticated user.

// Secure JavaScript code with CSRF token
document.getElementById('emailForm').addEventListener('submit', function(e) {
e.preventDefault();

let newEmail = document.getElementById('newEmail').value;
let csrfToken = document.getElementById('csrfToken').value; // Get the CSRF token from a hidden form field

fetch('/change-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken // Include the CSRF token in the request header
},
body: JSON.stringify({ email: newEmail }),
credentials: 'include'
});
});

In this secured example, a CSRF token is included in the request. The server can then verify this token before processing the request, ensuring that the request is legitimate and intentional by the user. If the token doesn’t match what’s expected, the request can be rejected, thwarting the CSRF attack.

By implementing anti-CSRF tokens and ensuring that every state-changing request from the client includes this token, developers can effectively mitigate the risk of CSRF attacks and protect user data and actions within their applications.

4. Insecure Direct Object References (IDOR)

Explanation of IDOR

Insecure Direct Object References (IDOR) occur when an application provides direct access to objects based on user-supplied input. This issue allows attackers to bypass authorization and access data and functionality directly, such as database records or files, by altering the value of a parameter used to directly point to an object.

Potential Impact of IDOR

The impact of IDOR can be significant, as it might allow an attacker to access sensitive data like personal information, financial details, or confidential documents. It can also let attackers modify or delete this data, leading to data breaches, privacy violations, and even financial loss.

JavaScript Code Example Showing Vulnerability

Consider a web application that lets users view their bank account details via a direct reference (like an account number in the URL):

// JavaScript code simulating a request to view account details - vulnerable to IDOR
function viewAccountDetails(accountNumber) {
let url = `https://bank.com/account-details?accountNumber=${accountNumber}`;
window.location.href = url;
}

In this scenario, if the application doesn’t properly verify that the user has the right to access the account tied to the accountNumber, an attacker could simply change the accountNumber parameter in the URL to access someone else's account information.

JavaScript Code Example with Access Control Checks

To prevent IDOR, you must implement proper access control checks that verify whether the logged-in user has permission to access the requested object. Here’s how you might refactor the above code to include such checks:

// JavaScript code with access control check to prevent IDOR
function viewAccountDetails(accountNumber, userSession) {
fetch(`/verify-access?accountNumber=${accountNumber}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${userSession.token}`
}
})
.then(response => response.json())
.then(data => {
if (data.hasAccess) {
let url = `https://bank.com/account-details?accountNumber=${accountNumber}`;
window.location.href = url;
} else {
alert('You do not have access to view this account.');
}
});
}

In this improved example, the application makes a call to a server-side function (/verify-access) to check whether the current user session has the right to access the account details for the specified accountNumber. If the user has the necessary permissions, they can view the account details; otherwise, they receive an access denial message.

By ensuring that every direct object reference is checked against the user’s permissions, you can prevent unauthorized access and secure your application against IDOR vulnerabilities.

5. Security Misconfiguration

Explanation of Security Misconfiguration

Security Misconfiguration is a broad term that encompasses various types of vulnerabilities arising from improper setup or management of technology. This can happen at any level of an application stack, including the network, web server, database, application, and code. It often results from default configurations, incomplete or ad-hoc configurations, open cloud storage, unnecessary services running, or outdated and vulnerable software.

The potential impact of security misconfiguration can be severe, including data breaches, unauthorized access, and loss of service. For example, a misconfigured database may expose sensitive information to the public internet, or a misconfigured web server might allow attackers to gain unauthorized access to administrative panels.

JavaScript Code Example: Exploiting Misconfiguration

Let’s consider a web application that misconfigures session management, allowing sessions to remain active indefinitely:

// Example of a misconfigured session timeout in a Node.js application
app.use(session({
secret: 'my_secret_key',
cookie: { maxAge: null } // Misconfiguration: Session never expires
}));

In this scenario, once a user logs in, their session remains active indefinitely, which can be exploited if the session ID is stolen or leaked.

JavaScript Code Example: Preventing Misconfiguration with Access Control

A better approach involves configuring the session management properly to avoid indefinite sessions and ensuring that only authorized users can access certain functionalities:

// Example of a properly configured session timeout in a Node.js application
app.use(session({
secret: 'my_secret_key',
cookie: { maxAge: 3600000 } // Sessions expire after 1 hour
}));

// Example of using access control to prevent security misconfiguration
app.get('/admin', (req, res) => {
if (!req.user.isAdmin) { // Access control check
return res.status(403).send('Access Denied');
}
res.send('Welcome to Admin Panel');
});

In the corrected example, the session is configured to expire after one hour, reducing the risk of session hijacking. Additionally, an access control check is implemented to ensure that only users with admin privileges can access the admin panel. This prevents unauthorized access, a common issue in security misconfiguration scenarios.

By understanding the nature of security misconfiguration and implementing proper security controls and configurations, organizations can protect their applications and data from potential threats.

Use of Components with Known Vulnerabilities

The Risks of Using Third-Party Components with Known Vulnerabilities

Third-party components, such as libraries, frameworks, and other software modules, are integral to modern software development. However, using components with known vulnerabilities can expose software systems to significant risks. Attackers can exploit these vulnerabilities to compromise systems, steal sensitive data, or disrupt services. The reliance on these components means that a single vulnerability can have a widespread impact, affecting multiple applications and users.

How to Ensure Your Components Are Up to Date and Secure

  1. Regularly Update Components: Keep all third-party components up to date with the latest versions. Developers often release updates to patch known vulnerabilities.
  2. Vulnerability Scanning: Use automated tools to scan your codebase for known vulnerabilities in the components you use. Tools like OWASP Dependency-Check, Snyk, or WhiteSource can identify insecure libraries and frameworks.
  3. Read Security Advisories: Stay informed about any security advisories related to the components you use. This can often be facilitated through subscribing to mailing lists or following repositories on platforms like GitHub.
  4. Minimize Third-Party Dependencies: Only use necessary components to reduce the attack surface. Evaluate the security posture of third-party components before incorporating them into your projects.
  5. Implement Robust Security Controls: Ensure that your application has strong security controls to mitigate the potential exploitation of vulnerabilities in third-party components.

Tools and Practices for Dependency Management

  • Dependency Management Tools: Utilize tools that can manage and analyze dependencies in your projects. For example, npm for Node.js, Maven for Java, or pip for Python, all provide ways to manage third-party packages and can be integrated with security scanning tools.
  • Automated Security Patching: Tools like Dependabot can automatically create pull requests to update dependencies when a new version is available, especially if it fixes a known vulnerability.
  • Software Composition Analysis (SCA): SCA tools can provide a detailed analysis of the open-source components used in your software, identifying known vulnerabilities and licensing issues.
  • Continuous Integration/Continuous Deployment (CI/CD) Pipelines: Integrate dependency checks and security scans into your CI/CD pipelines. This ensures that vulnerabilities are detected and addressed early in the development lifecycle.
  • Policy and Compliance Checks: Establish policies for using third-party components, such as requiring a security review before inclusion, and perform regular audits for compliance.

By proactively managing third-party dependencies and staying informed about potential vulnerabilities, software developers and engineers can significantly reduce the risks associated with using components with known vulnerabilities. This not only protects the integrity of the software but also maintains the trust of its users.

Conclusion

In the digital era, prioritizing security in software development is crucial. It’s not merely about defending data but about nurturing trust and sustaining business continuity. Security should be embedded from the start, minimizing risks and costs associated with post-deployment fixes.

Creating a security-conscious culture within development teams is essential. This involves continuous education on security practices, collaborative efforts between security and development teams, and integrating security checks into the development lifecycle. Automated tools and CI/CD pipelines play a pivotal role in maintaining consistent security standards.

Ultimately, emphasizing security in software development is about safeguarding the digital ecosystem while ensuring that software remains reliable and trustworthy for users. By embedding security at the core of development processes and fostering a vigilant team culture, organizations can build robust software solutions that withstand evolving cyber threats.

--

--

Sandalikaariyarathna
SLIIT Women In FOSS Community

Passionate about software development, I'm a quick-learner and dedicated supporter, eager to conquer new dimensions and deliver exceptional outcomes.