paint-brush
A Handy Checklist of Secure Coding Practices: Protect Your App and Your Usersby@shubhintech
720 reads
720 reads

A Handy Checklist of Secure Coding Practices: Protect Your App and Your Users

by Shubhdeep ChhabraSeptember 16th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this brief article, we will discuss important security measures that can protect web applications from potential threats and ensure the safety
featured image - A Handy Checklist of Secure Coding Practices: Protect Your App and Your Users
Shubhdeep Chhabra HackerNoon profile picture

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 practicing 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.


  1. Preventing SQL Injection
  2. Preventing Cross-Site Scripting (XSS)
  3. Testing Security with Selenium
  4. Secure Password Storage and Handling
  5. Cross-Site Request Forgery (CSRF) Protection
  6. Secure Session Management
  7. Content Security Policy (CSP) Implementation
  8. Handling Error Messages Securely
  9. Secure File Uploads
  10. 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.


A bad example is 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
})


A good example is 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 the 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.


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 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 in the next blog.

If you are reading this blog till here, then do follow me on Twitter — @ShubhInTech

Bye.


Also published here.