According to recent studies, over 80% of data breaches can be prevented by implementing improved secure coding practices. As such, it is imperative to incorporate these practices into your organization's software development processes.
In software engineering, it's not only about improving performance and optimizing processes. Ensuring security and practising secure coding is also crucial, regardless of your role as a developer, quality assurance specialist, or cloud engineer. Security should always be a top priority.
And, that is what we will be discussing today in this article.
Here are some common secure coding practices that you can use in your daily work when creating a new feature or developing an app.
- Preventing SQL Injection
- Preventing Cross-Site Scripting (XSS)
- Testing Security with Selenium
- Secure Password Storage and Handling
- Cross-Site Request Forgery (CSRF) Protection
- Secure Session Management
- Content Security Policy (CSP) Implementation
- Handling Error Messages Securely
- Secure File Uploads
- Logging and Auditing for Security Monitoring
Let's start
Preventing SQL Injection
SQL injection is a common way for malicious actors to attack web applications by taking advantage of poorly sanitized user inputs to manipulate the database. This vulnerability can lead to unauthorized access to sensitive information, alteration or deletion of data, and even administrative control of the application. It is crucial to address this issue to maintain data integrity, build user trust, and prevent serious security breaches. Ignoring this vulnerability could result in data leaks, unauthorized access, and harm to your application's reputation.
Bad Example of not following secure coding practice
/**
Here user input (userId) is directly interpolated into the SQL query,
leaving the application vulnerable to SQL injection.
*/
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.query(query, (err, result) => {
// Process the query result
});
Good Example of secure coding practice
/**
Here parameterized queries are used, which ensures proper handling of
user input, preventing SQL injection attacks.
*/
const userId = req.params.id;
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId], (err, result) => {
// Process the query result
});
Cross-Site Scripting (XSS)
It's really important to make sure that web applications are protected from Cross-Site Scripting (XSS) attacks for your safety. These attacks can let hackers inject malicious scripts into web apps, which then get executed in the browsers of unsuspecting users. This can lead to all sorts of bad things like data theft, financial loss, and unauthorized access to your accounts. If you don't take steps to secure your frontend, you might end up losing the trust of your users and even facing legal consequences.
Bad Example of not following secure coding practice
import React, { useState } from 'react';
const Component = () => {
const [inputText, setInputText] = useState('');
const [htmlString, setHtmlString] = useState('');
const handleInputChange = (e) => {
setInputText(e.target.value);
};
const handleHtmlStringSubmit = () => {
// inputText is used directly.
setHtmlString(`<div>${inputText}</div>`);
};
return (
<div>
<input type='text' value={inputText} onChange={handleInputChange} />
<button onClick={handleHtmlStringSubmit}>Submit</button>
<div>
<h3>HTML:</h3>
<div dangerouslySetInnerHTML={{ __html: htmlString }} />
</div>
</div>
);
};
export default Component;
Good Example following secure code practice
import React, { useState } from 'react';
const Component = () => {
const [inputText, setInputText] = useState('');
const [htmlString, setHtmlString] = useState('');
const handleInputChange = (e) => {
setInputText(e.target.value);
};
const handleHtmlStringSubmit = () => {
setHtmlString(`<div>${inputText}</div>`);
};
return (
<div>
<input type='text' value={inputText} onChange={handleInputChange} />
<button onClick={handleHtmlStringSubmit}>Submit</button>
<div>
<h3>HTML:</h3>
{/* htmlString is sanitized properly */}
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(htmlString) }} />
</div>
</div>
);
};
export default Component;
Testing Security with Selenium
-
Security testing is a crucial aspect of software development that allows us to identify and address potential vulnerabilities and weaknesses in our applications.
-
It involves simulating real-world attack scenarios to assess how well our defenses hold up against malicious intent.
-
In our talk, we've already explored secure coding practices to secure our code against common threats like SQL injection and Cross-Site Scripting (XSS). Now, with Selenium, we venture into the realm of automated testing, empowering us to conduct thorough and repeatable security tests.
Example with Security Testing taken into consideration
import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; public class LoginTestGood { public static void main(String[] args) { WebDriver driver = new ChromeDriver(); try { // Step 1: Open the login page driver.get("https://example.com/login"); // Step 2: Perform security test for SQL injection String maliciousInput = "'; DROP TABLE users; --"; // Simulating SQL injection payload WebElement usernameField = driver.findElement(By.name("username")); WebElement passwordField = driver.findElement(By.name("password")); usernameField.sendKeys(maliciousInput); passwordField.sendKeys("secret123"); passwordField.submit(); // Step 3: Wait for the login to complete (here we wait for an element on the login page to check for failure) WebDriverWait wait = new WebDriverWait(driver, 10); wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".login-form"))); System.out.println("Test (good) - SQL Injection attempt detected"); } catch (Exception e) { System.err.println("Test (good) - Security test passed: " + e.getMessage()); } finally { driver.quit(); } } }
Secure Password Storage and Handling
It is essential to store and handle passwords correctly to safeguard user accounts from unauthorized access, regardless of whether the application's database is compromised.
Improper password storage practices, like storing passwords in plain text or weakly hashed formats, can result in password leaks that compromise user safety.
Bad example where we are storing passwords directly in DB and this can compromise the security of our app.
app.post('/register', (req, res) => {
const username = req.body.username;
const password = req.body.password;
// Store the user in the database with the password as-is
});
Good example where the password is encrypted and stored in DB.
const bcrypt = require('bcrypt');
app.post('/register', async (req, res) => {
const username = req.body.username;
const password = req.body.password;
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Store the user in the database with the hashedPassword
});
Cross-Site Request Forgery (CSRF) Protection
CSRF is an attack that tricks users into submitting unintended requests by exploiting their authenticated session.
Proper CSRF protection is essential to prevent attackers from performing unauthorized actions on behalf of the user.
Without CSRF protection, attackers can execute malicious actions on behalf of authenticated users, leading to account compromise and unauthorized data manipulation.
Bad Example where a form is created for changing passwords without the support of a CSRF token.
import { useState } from 'react';
const PasswordChangeForm = () => {
const [newPassword, setNewPassword] = useState('');
const handleChange = (event) => {
setNewPassword(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
// Send the password change request without CSRF token
fetch('/change_password', {
method: 'POST',
body: JSON.stringify({ newPassword }),
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => response.json())
.then((data) => {
console.log(data);
// Handle response data
})
.catch((error) => {
console.error('Error:', error);
// Handle error
});
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor='newPassword'>New Password:</label>
<input type='password' id='newPassword' name='newPassword' value={newPassword} onChange={handleChange} />
<button type='submit'>Change Password</button>
</form>
);
};
export default PasswordChangeForm;
Good Example where the support of CSRF token is provided for better security.
import { useState } from 'react';
const PasswordChangeForm = ({ csrfToken }) => {
const [newPassword, setNewPassword] = useState('');
const handleChange = (event) => {
setNewPassword(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
// Send the password change request with CSRF token
fetch('/change_password', {
method: 'POST',
body: JSON.stringify({ newPassword }),
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken, // Include CSRF token in the request header
},
})
.then((response) => response.json())
.then((data) => {
console.log(data);
// Handle response data
})
.catch((error) => {
console.error('Error:', error);
// Handle error
});
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor='newPassword'>New Password:</label>
<input type='password' id='newPassword' name='newPassword' value={newPassword} onChange={handleChange} />
<button type='submit'>Change Password</button>
</form>
);
};
export default PasswordChangeForm;
const express = require('express');
const bodyParser = require('body-parser');
const csurf = require('csurf');
const app = express();
app.use(bodyParser.json());
// Generate and include CSRF token for all responses
app.use(csurf());
// Middleware to expose CSRF token to the frontend
app.use((req, res, next) => {
res.cookie('XSRF-TOKEN', req.csrfToken());
next();
});
app.post('/change_password', (req, res) => {
// Validate CSRF token before processing the request
if (req.headers['csrf-token'] !== req.csrfToken()) {
return res.status(403).json({ error: 'Invalid CSRF token.' });
}
const newPassword = req.body.newPassword;
// Process password change logic
// ...
res.json({ message: 'Password changed successfully.' });
});
app.listen(3000, () => {
console.log('Server started on port 3000.');
});
Secure Session Management
Secure Session Management is essential to prevent unauthorized access and session hijacking.
If session management is insecure, it can lead to session hijacking, which allows attackers to impersonate users and gain access to sensitive information or perform actions on their behalf.
Bad Example where no session management is done
// Insecure session management using just plain HTTP cookies
app.get('/dashboard', (req, res) => {
const user = findUserById(req.cookies.userId);
// Display user dashboard
});
Good Example where Redis is used for session management making the system more secure.
// Secure session management
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const sessionConfig = {
store: new RedisStore({ url: 'redis://localhost:6379' }),
secret: 'secret_key',
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
maxAge: 3600000,
},
};
app.use(session(sessionConfig));
app.get('/dashboard', (req, res) => {
const user = findUserById(req.session.userId);
// Display user dashboard
});
Content Security Policy (CSP) Implementation
The Content Security Policy (CSP) plays a vital role in web application security by preventing Cross-Site Scripting (XSS) attacks. It does this by regulating the resources that are allowed to be loaded and executed on a web page.
CSP accomplishes this by setting limits on which sources can be used for scripts, styles, fonts, images, and other resources, thereby reducing the potential risks of XSS vulnerabilities.
Bad example where React app does not include any Content Security Policy (CSP), leaving it vulnerable to various types of attacks, including Cross-Site Scripting (XSS).
const ExampleApp = () => {
return (
<div>
<h1>Welcome to My App</h1>
<p>This is a vulnerable React application.</p>
<script src='https://evil.com/malicious.js'></script>
</div>
);
};
export default ExampleApp;
Good Example where the server includes a CSP middleware that sets the "Content-Security-Policy" header in the HTTP response. The CSP is configured to only allow script sources from the same origin ('self'), the trusted CDN domain ('https://trusted-cdn.com'), and scripts with the matching nonce value ('nonce-randomly_generated_nonce').
const ExampleApp = () => {
return (
<div>
<h1>Welcome to My Secure App</h1>
<p>This is a secure React application with CSP.</p>
<script src='https://trusted-cdn.com/app.js' nonce='randomly_generated_nonce'></script>
</div>
);
};
export default ExampleApp;
const express = require('express');
const app = express();
// CSP middleware to set the Content-Security-Policy header
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"script-src 'self' 'nonce-randomly_generated_nonce' https://trusted-cdn.com; default-src 'self'"
);
next();
});
// Serve the React application
app.use(express.static('build'));
app.listen(3000, () => {
console.log('Server started on port 3000.');
});
Handling Error Messages Securely
When an application encounters issues, error messages can offer useful feedback to both users and developers. However, if these messages are not handled properly, they can reveal sensitive information or help attackers identify potential vulnerabilities.
Bad Example where the whole user object is sent in response.
app.get('/user/:id', (req, res) => {
const userId = req.params.id;
const user = getUserById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
Good Example where only necessary details are sent in exposed.
app.get('/user/:id', (req, res) => {
const userId = req.params.id;
const user = getUserById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ username: user.username, email: user.email });
});
Secure File Uploads
Users can use file upload functionality to share data with the application. However, if this feature is not handled securely, it can become a vector for various attacks, such as code execution or denial of service.
Insecure file uploads can have serious consequences, such as remote code execution or the storage of malicious files on the server.
Therefore, it is crucial to implement secure file upload practices to prevent potential threats and maintain the integrity of the application.
Here is the Bad Example where the application allows users to upload files without proper validation and handling. The uploaded files are stored in the "uploads" directory, making them publicly accessible, which is a security risk. Additionally, the application does not check the file type or perform any security checks, leaving it vulnerable to malicious file uploads that may contain harmful scripts or malware.
const express = require('express');
const app = express();
const multer = require('multer');
app.use(express.static('uploads')); // Exposing the uploads directory
// Insecure file upload handling
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // Uploading files to the "uploads" directory
},
filename: (req, file, cb) => {
cb(null, file.originalname); // Using the original filename
},
});
const upload = multer({ storage });
app.post('/upload', upload.single('file'), (req, res) => {
// File processing logic (e.g., saving to the database)
// ...
res.send('File uploaded successfully.');
});
app.listen(3000, () => {
console.log('Server started on port 3000.');
});
Good Example where the application implements secure file upload practices, including proper validation, file type checking, and storing files in a protected directory.
const express = require('express');
const app = express();
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
// Secure file upload handling
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // Uploading files to the "uploads" directory
},
filename: (req, file, cb) => {
// Generate a unique filename to prevent overwriting
const uniqueFilename = crypto.randomBytes(16).toString('hex');
const fileExtension = path.extname(file.originalname);
cb(null, uniqueFilename + fileExtension);
},
});
const fileFilter = (req, file, cb) => {
// Allow only specific file types (e.g., images)
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png' || file.mimetype === 'image/gif') {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only images (jpeg, png, gif) are allowed.'));
}
};
const upload = multer({ storage, fileFilter });
app.post('/upload', upload.single('file'), (req, res) => {
// File processing logic (e.g., saving to the database)
// ...
res.send('File uploaded successfully.');
});
app.listen(3000, () => {
console.log('Server started on port 3000.');
});
Logging and Auditing for Security Monitoring
Logging and auditing are crucial components of security monitoring, providing a detailed record of activities within the application. Properly implemented, they aid in detecting and responding to security incidents promptly.
By utilizing security monitoring, developers and administrators can detect suspicious activities, unauthorized access attempts, and possible security breaches. However, without thorough logging and auditing, it can be difficult to effectively identify and investigate security incidents.
Bad Example
// Inadequate logging and auditing mechanisms
app.post('/data', (req, res) => {
const sensitiveData = req.body.data;
// Process data without logging relevant events
});
Good Example where there is appropriate logging of sensitive actions and events, facilitating security monitoring and incident response.
const fs = require('fs');
// Logging sensitive actions and events
app.post('/data', (req, res) => {
const sensitiveData = req.body.data;
// Process data and log relevant events
fs.appendFile('security.log', `Data processed: ${sensitiveData}\n`, (err) => {
if (err) {
console.error('Error writing to log file:', err);
}
});
});
And, that's it.
These were some secure coding practices, which if incorporated into code we can definitely protect our app and the users.
I hope you find this helpful. 🙌
On that note, I’ll take a leave until the next blog.
We will learn something new next blog.
If you are reading this blog till here then do follow me on X — @ShubhInTech
Bye.