Input validation testing - WSTG-INPV

Input validation testing - WSTG-INPV

·

8 min read

WSTG-INPV (Input Validation Testing), which is a section of the OWASP Web Security Testing Guide (WSTG). Input validation testing is a critical component of web application security testing that focuses on how applications handle user inputs.

The OWASP WSTG Input Validation Testing section typically covers testing methodologies for various input validation vulnerabilities, including:

  1. SQL Injection

  2. Cross-site Scripting (XSS)

  3. HTTP Parameter Pollution

  4. Command Injection

  5. Server-side Template Injection

  6. XML Injection

  7. SSI Injection

  8. LDAP Injection

  9. ORM Injection

  10. Buffer Overflows

These tests help security professionals identify weaknesses in how applications validate, filter, sanitize, and process user inputs, which are common attack vectors for exploiting web applications.

Input validators:

// input-validators.ts
/**
 * Next.js 14 Input Validation Library
 * Based on OWASP WSTG-INPV (Input Validation Testing) categories
 */

// Type definitions for validation results
type ValidationResult = {
  isValid: boolean;
  message?: string;
};

/**
 * SQL Injection Prevention
 * Validates input against common SQL injection patterns
 */
export function validateSqlInjection(input: string): ValidationResult {
  if (!input) return { isValid: true };

  // Check for common SQL injection patterns
  const sqlPatterns = [
    /('|"|;|--|\/\*|\*\/|@@|@|\bAND\b|\bOR\b|\bUNION\b|\bSELECT\b|\bFROM\b|\bWHERE\b|\bDROP\b|\bTABLE\b|\bINSERT\b|\bDELETE\b|\bUPDATE\b)/i,
    /(ALTER|CREATE|DELETE|DROP|EXEC(UTE){0,1}|INSERT( +INTO){0,1}|MERGE|SELECT|UPDATE|UNION( +ALL){0,1})/i
  ];

  for (const pattern of sqlPatterns) {
    if (pattern.test(input)) {
      return {
        isValid: false,
        message: "Potential SQL injection detected"
      };
    }
  }

  return { isValid: true };
}

/**
 * Cross-site Scripting (XSS) Prevention
 * Checks for potential XSS attacks in input
 */
export function validateXSS(input: string): ValidationResult {
  if (!input) return { isValid: true };

  // Check for common XSS patterns
  const xssPatterns = [
    /<script[^>]*>.*?<\/script>/is,
    /javascript:[^\s]*/i,
    /onerror\s*=|onload\s*=|onclick\s*=|onmouseover\s*=/i,
    /<iframe[^>]*>/i,
    /<img[^>]*>/i
  ];

  for (const pattern of xssPatterns) {
    if (pattern.test(input)) {
      return {
        isValid: false,
        message: "Potential XSS attack detected"
      };
    }
  }

  return { isValid: true };
}

/**
 * Command Injection Prevention
 * Validates against OS command injection attempts
 */
export function validateCommandInjection(input: string): ValidationResult {
  if (!input) return { isValid: true };

  // Check for common command injection patterns
  const cmdPatterns = [
    /[;&|`\(\)$]/,
    /\b(cat|cd|chmod|curl|echo|exec|find|grep|kill|ls|mkdir|mv|nc|ping|rm|sh|sleep|touch|wget)\b/i
  ];

  for (const pattern of cmdPatterns) {
    if (pattern.test(input)) {
      return {
        isValid: false,
        message: "Potential command injection detected"
      };
    }
  }

  return { isValid: true };
}

/**
 * HTTP Parameter Pollution Prevention
 * Validates for duplicate parameters that could lead to HPP
 */
export function validateHPP(params: Record<string, string[]>): ValidationResult {
  for (const [key, values] of Object.entries(params)) {
    if (values.length > 1) {
      return {
        isValid: false,
        message: `HTTP Parameter Pollution detected for parameter: ${key}`
      };
    }
  }

  return { isValid: true };
}

/**
 * Server-side Template Injection Prevention
 * Validates against template injection patterns
 */
export function validateTemplateInjection(input: string): ValidationResult {
  if (!input) return { isValid: true };

  // Check for common template injection patterns
  const templatePatterns = [
    /\$\{.*?\}/,
    /\{\{.*?\}\}/,
    /#\{.*?\}/,
    /<\%.*?\%>/
  ];

  for (const pattern of templatePatterns) {
    if (pattern.test(input)) {
      return {
        isValid: false,
        message: "Potential template injection detected"
      };
    }
  }

  return { isValid: true };
}

/**
 * XML Injection Prevention
 * Validates XML input against common XML injection patterns
 */
export function validateXMLInjection(input: string): ValidationResult {
  if (!input) return { isValid: true };

  // Check for XML injection patterns
  const xmlPatterns = [
    /<!DOCTYPE|<!ENTITY|<!ELEMENT/i,
    /<!\[CDATA\[.*?\]\]>/i,
    /\bXXE\b/i
  ];

  for (const pattern of xmlPatterns) {
    if (pattern.test(input)) {
      return {
        isValid: false,
        message: "Potential XML injection detected"
      };
    }
  }

  return { isValid: true };
}

/**
 * SSI Injection Prevention
 * Validates against Server-Side Include injection patterns
 */
export function validateSSIInjection(input: string): ValidationResult {
  if (!input) return { isValid: true };

  // Check for SSI injection patterns
  const ssiPatterns = [
    /<!--#include|<!--#exec|<!--#echo|<!--#config|<!--#flastmod|<!--#fsize/i
  ];

  for (const pattern of ssiPatterns) {
    if (pattern.test(input)) {
      return {
        isValid: false,
        message: "Potential SSI injection detected"
      };
    }
  }

  return { isValid: true };
}

/**
 * LDAP Injection Prevention
 * Validates against LDAP injection patterns
 */
export function validateLDAPInjection(input: string): ValidationResult {
  if (!input) return { isValid: true };

  // Check for LDAP injection patterns
  const ldapPatterns = [
    /[()&|!*]/,
    /\)(cn|ou|dc|o)=/i
  ];

  for (const pattern of ldapPatterns) {
    if (pattern.test(input)) {
      return {
        isValid: false,
        message: "Potential LDAP injection detected"
      };
    }
  }

  return { isValid: true };
}

/**
 * ORM Injection Prevention
 * Validates against ORM injection patterns (like NoSQL injection)
 */
export function validateORMInjection(input: string): ValidationResult {
  if (!input) return { isValid: true };

  // Check for NoSQL injection patterns
  const ormPatterns = [
    /\$where|findOne|find\(/i,
    /{.*\$ne.*}|{.*\$gt.*}|{.*\$lt.*}|{.*\$exists.*}/i
  ];

  for (const pattern of ormPatterns) {
    if (pattern.test(input)) {
      return {
        isValid: false,
        message: "Potential ORM/NoSQL injection detected"
      };
    }
  }

  return { isValid: true };
}

/**
 * General Input Length Validation
 * Ensures input is within acceptable length limits
 */
export function validateInputLength(input: string, maxLength: number = 1000): ValidationResult {
  if (!input) return { isValid: true };

  if (input.length > maxLength) {
    return {
      isValid: false,
      message: `Input exceeds maximum length of ${maxLength} characters`
    };
  }

  return { isValid: true };
}

/**
 * Comprehensive input validation
 * Runs all validators on a single input
 */
export function validateInput(input: string): ValidationResult {
  const validators = [
    validateSqlInjection,
    validateXSS,
    validateCommandInjection,
    validateTemplateInjection,
    validateXMLInjection,
    validateSSIInjection,
    validateLDAPInjection,
    validateORMInjection
  ];

  for (const validator of validators) {
    const result = validator(input);
    if (!result.isValid) {
      return result;
    }
  }

  return { isValid: true };
}

/**
 * Sanitizes user input by removing potentially harmful characters
 */
export function sanitizeInput(input: string): string {
  if (!input) return '';

  // Remove script tags and potentially harmful HTML
  let sanitized = input
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
    .replace(/javascript:/gi, 'removed:')
    .replace(/on\w+=/gi, 'data-removed=');

  // Encode HTML entities
  sanitized = sanitized
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');

  return sanitized;
}

These validators in a Next.js 14 API route:

// app/api/user/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { validateInput, sanitizeInput } from '@/utils/input-validators';

export async function POST(request: NextRequest) {
  try {
    // Parse the request body
    const body = await request.json();

    // Validate each input field
    const fields = ['username', 'email', 'comment'];

    for (const field of fields) {
      if (body[field]) {
        const validation = validateInput(body[field]);

        if (!validation.isValid) {
          return NextResponse.json({
            success: false,
            error: `${validation.message} in field '${field}'`
          }, { status: 400 });
        }

        // Sanitize the input
        body[field] = sanitizeInput(body[field]);
      }
    }

    // Process valid input...
    // For example, save to database

    return NextResponse.json({
      success: true,
      message: 'Data processed successfully'
    });

  } catch (error) {
    console.error('Error processing request:', error);
    return NextResponse.json({
      success: false,
      error: 'Invalid request format'
    }, { status: 400 });
  }
}

Here's how you can use these validators with a form component:

// components/SecureForm.tsx

'use client';

import { useState, FormEvent } from 'react';
import { validateInput } from '@/utils/input-validators';

export default function SecureForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    comment: ''
  });

  const [errors, setErrors] = useState<Record<string, string>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [message, setMessage] = useState('');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));

    // Real-time validation
    const validation = validateInput(value);
    if (!validation.isValid) {
      setErrors(prev => ({ ...prev, [name]: validation.message || 'Invalid input' }));
    } else {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[name];
        return newErrors;
      });
    }
  };

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    setMessage('');

    // Validate all fields before submission
    let hasErrors = false;
    const newErrors: Record<string, string> = {};

    Object.entries(formData).forEach(([field, value]) => {
      const validation = validateInput(value);
      if (!validation.isValid) {
        newErrors[field] = validation.message || 'Invalid input';
        hasErrors = true;
      }
    });

    if (hasErrors) {
      setErrors(newErrors);
      setIsSubmitting(false);
      return;
    }

    try {
      const response = await fetch('/api/user', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });

      const data = await response.json();

      if (data.success) {
        setMessage('Form submitted successfully!');
        // Reset form
        setFormData({ username: '', email: '', comment: '' });
      } else {
        setMessage(`Error: ${data.error}`);
      }
    } catch (error) {
      setMessage('Failed to submit form. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
      <h2 className="text-xl font-bold mb-4">Secure Form</h2>

      {message && (
        <div className={`p-3 mb-4 rounded ${message.includes('Error') ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
          {message}
        </div>
      )}

      <form onSubmit={handleSubmit}>
        <div className="mb-4">
          <label htmlFor="username" className="block mb-1 font-medium">
            Username
          </label>
          <input
            type="text"
            id="username"
            name="username"
            value={formData.username}
            onChange={handleChange}
            className={`w-full p-2 border rounded ${errors.username ? 'border-red-500' : 'border-gray-300'}`}
            required
          />
          {errors.username && (
            <p className="mt-1 text-sm text-red-600">{errors.username}</p>
          )}
        </div>

        <div className="mb-4">
          <label htmlFor="email" className="block mb-1 font-medium">
            Email
          </label>
          <input
            type="email"
            id="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            className={`w-full p-2 border rounded ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
            required
          />
          {errors.email && (
            <p className="mt-1 text-sm text-red-600">{errors.email}</p>
          )}
        </div>

        <div className="mb-4">
          <label htmlFor="comment" className="block mb-1 font-medium">
            Comment
          </label>
          <textarea
            id="comment"
            name="comment"
            value={formData.comment}
            onChange={handleChange}
            className={`w-full p-2 border rounded ${errors.comment ? 'border-red-500' : 'border-gray-300'}`}
            rows={4}
            required
          />
          {errors.comment && (
            <p className="mt-1 text-sm text-red-600">{errors.comment}</p>
          )}
        </div>

        <button
          type="submit"
          disabled={isSubmitting || Object.keys(errors).length > 0}
          className="w-full py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-blue-300"
        >
          {isSubmitting ? 'Submitting...' : 'Submit'}
        </button>
      </form>
    </div>
  );
}

How to Use This Code

  1. Setup:

    • Create a utils folder in your Next.js 14 project

    • Add the input-validators.ts file to this folder

    • Create your API routes and form components as shown in the examples

  2. Key Features:

    • Comprehensive validation against all OWASP WSTG-INPV vulnerability types

    • Client-side validation with real-time feedback

    • Server-side validation as a security best practice

    • Input sanitization to remove potentially harmful content

    • Detailed error messages for developers and users

  3. Security Best Practices:

    • Always validate inputs on both client and server sides

    • Sanitize inputs before storing or processing them

    • Use specific error messages for development, but consider using generic messages in production

    • Implement rate limiting and CSRF protection (not shown in these examples)