DEV Community

Wallace Freitas
Wallace Freitas

Posted on

Techniques for Writing Secure Code: Safeguarding Your Software from Vulnerabilities

Writing secure code is essential to preventing vulnerabilities in your program in an era where cyber threats are getting more complex. Making sure your code is secure preserves user safety and confidence while also protecting your application. These are some basic secure code development practices that all developers should follow, along with TypeScript examples to help you understand each one.

1. Input Validation and Sanitization

Input validation involves verifying that user inputs are as expected before processing them. Sanitization ensures that inputs are cleaned of any potentially harmful data.

Techniques:

✓ Whitelist Validation: Accept only known good inputs. Define and enforce rules for what constitutes acceptable data.

✓ Sanitize Inputs: Strip out or escape any malicious characters or scripts from user inputs to prevent injection attacks.

✓ Use Built-in Functions: Utilize built-in functions and libraries designed for input validation and sanitization, such as OWASP ESAPI.

💡 Example:

function validateEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
}

function sanitizeInput(input: string): string {
    return input.replace(/[<>]/g, "");
}

const userEmail = "<script>alert('xss')</script>user@example.com";
if (validateEmail(userEmail)) {
    const safeEmail = sanitizeInput(userEmail);
    console.log(safeEmail); // Output: alert('xss')user@example.com
} else {
    console.log("Invalid email format.");
}
Enter fullscreen mode Exit fullscreen mode

2. Use Parameterized Queries

Parameterized queries ensure that SQL queries are executed with parameters, rather than including user inputs directly into the query string.

Techniques:

✓ Prepared Statements: Use prepared statements for database queries to separate SQL logic from data.

✓ ORMs: Employ Object-Relational Mappers (ORMs) which often provide built-in protection against SQL injection attacks.

💡 Example (using TypeORM):

import { getRepository } from "typeorm";
import { User } from "./entity/User";

async function findUserByUsername(username: string): Promise<User | undefined> {
    const userRepository = getRepository(User);
    return await userRepository.findOne({ where: { username } });
}

const username = "john_doe";
findUserByUsername(username).then(user => {
    console.log(user);
});
Enter fullscreen mode Exit fullscreen mode

3. Implement Proper Authentication and Authorization

Authentication verifies the identity of users, while authorization determines their permissions.

Techniques:

✓ Strong Password Policies: Enforce strong password policies and use hashing algorithms (e.g., bcrypt) to store passwords securely.

✓ Multi-Factor Authentication (MFA): Implement MFA to add an additional layer of security.

✓ Role-Based Access Control (RBAC): Use RBAC to ensure users have access only to what they need for their roles.

💡 Example (using bcrypt and JWT):

import * as bcrypt from "bcrypt";
import * as jwt from "jsonwebtoken";

const saltRounds = 10;
const secretKey = "your_secret_key";

async function hashPassword(password: string): Promise<string> {
    return await bcrypt.hash(password, saltRounds);
}

async function comparePassword(password: string, hash: string): Promise<boolean> {
    return await bcrypt.compare(password, hash);
}

function generateToken(userId: string): string {
    return jwt.sign({ userId }, secretKey, { expiresIn: "1h" });
}

const password = "securePassword123";
hashPassword(password).then(hash => {
    console.log("Hashed password:", hash);
});
Enter fullscreen mode Exit fullscreen mode

4. Secure Data Storage and Transmission

Ensuring data is stored and transmitted securely to prevent unauthorized access.

Techniques:

✓ Encryption: Use strong encryption (e.g., AES-256) for sensitive data both at rest and in transit.

✓ TLS/SSL: Implement TLS/SSL for all communications between the client and server to protect data in transit.

✓ Key Management: Use proper key management practices to store and rotate encryption keys securely.

💡 Example:

import * as crypto from "crypto";

const algorithm = "aes-256-cbc";
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);

function encrypt(text: string): string {
    const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv);
    let encrypted = cipher.update(text);
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    return iv.toString("hex") + ":" + encrypted.toString("hex");
}

function decrypt(text: string): string {
    const textParts = text.split(":");
    const iv = Buffer.from(textParts.shift()!, "hex");
    const encryptedText = Buffer.from(textParts.join(":"), "hex");
    const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv);
    let decrypted = decipher.update(encryptedText);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString();
}

const originalText = "Sensitive data";
const encryptedText = encrypt(originalText);
console.log("Encrypted:", encryptedText);
console.log("Decrypted:", decrypt(encryptedText));
Enter fullscreen mode Exit fullscreen mode

5. Secure Error Handling and Logging

Proper error handling and logging can prevent information leakage and help in detecting security issues.

Techniques:

✓ Generalized Error Messages: Provide users with generic error messages that do not reveal sensitive information.

✓ Detailed Logging: Log detailed error messages and system events for internal use to monitor and diagnose issues.

✓ Secure Log Storage: Protect log files from unauthorized access and tampering.

TypeScript Example:

import * as fs from "fs";

function logError(error: Error): void {
    const logMessage = `${new Date().toISOString()} - Error: ${error.message}\n`;
    fs.appendFileSync("error.log", logMessage);
}

function handleError(res: any, error: Error): void {
    logError(error);
    res.status(500).send("An unexpected error occurred. Please try again later.");
}

try {
    // Simulate an error
    throw new Error("Database connection failed.");
} catch (error) {
    handleError(response, error); // response is an HTTP response object
}
Enter fullscreen mode Exit fullscreen mode

6. Regular Security Audits and Code Reviews

Continuous evaluation of code and practices to identify and address potential security issues.

Techniques:

✓ Automated Tools: Use static and dynamic analysis tools to automatically scan code for vulnerabilities.

✓ Peer Reviews: Conduct regular peer code reviews to identify and fix security issues early.

✓ Third-Party Audits: Periodically engage third-party security experts to conduct comprehensive security audits.

💡 Example (using ESLint):

// .eslintrc.json
{
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": 2020,
        "sourceType": "module"
    },
    "rules": {
        "no-eval": "error",
        "no-implied-eval": "error",
        "no-unused-vars": "warn",
        "security/detect-object-injection": "error"
    },
    "plugins": ["security"]
}

// Install necessary packages
// npm install eslint eslint-plugin-security --save-dev

// Run ESLint
// npx eslint . --ext .ts
Enter fullscreen mode Exit fullscreen mode

7. Least Privilege Principle

Granting the minimum necessary access rights to users and processes to perform their functions.

Techniques:

✓ Restrict Permissions: Limit permissions based on user roles and regularly review access controls.

✓ Isolate Services: Run services and applications with the least privilege necessary, using separate accounts and environments.

💡 Example (using AWS IAM policy):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::your-bucket-name/*"
        },
        {
            "Effect": "Deny",
            "Action": [
                "s3:DeleteObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::your-bucket-name/*"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Understanding potential threats and putting in place strong defenses are essential components of writing secure code, which is a proactive and ongoing effort. Developers can greatly lower the risk of vulnerabilities in their program by employing parameterized queries, verifying inputs, enforcing appropriate authentication and authorization, protecting data, handling errors correctly, doing routine audits, and adhering to the least privilege principle. Using these methods contributes to the development of safe applications that safeguard the company and its consumers.

Adopt these procedures to improve the security of your code and create a more secure online community.

Top comments (0)