Skip to content

⚠️ MEDIUM: Input Validation Gaps - Potential Injection and Data Corruption Vulnerabilities #3589

@ryota-murakami

Description

@ryota-murakami

⚠️ MEDIUM: Input Validation Gaps - Potential Injection and Data Corruption Vulnerabilities

Problem Description

The NSX application lacks comprehensive input validation across API endpoints, creating potential vulnerabilities for injection attacks, data corruption, and unexpected behavior.

Current problematic patterns:

// server/routes/post.ts:99-120
router.post('/create', isAuthorized, async (req: Request, res: Response) => {
  const { title, body } = req.body  // ❌ No validation
  try {
    const post = await prisma.post.create({
      data: {
        title: title,  // ❌ Could be null, undefined, or malicious
        body: body,    // ❌ Could contain XSS or injection payloads
      },
    })
    res.status(201).json(post)
  } catch (error: unknown) {
    // Generic error handling
  }
})

Why This Is Dangerous

  1. SQL Injection: Malicious input could potentially affect database queries
  2. XSS Attacks: Unvalidated content could contain malicious scripts
  3. Data Corruption: Invalid data types could cause application crashes
  4. Resource Exhaustion: Large payloads could consume server resources
  5. Business Logic Bypass: Invalid data could bypass intended constraints

Current Impact

  • Severity: Medium-High
  • Attack Vectors: Injection, XSS, DoS, data corruption
  • Risk Level: Medium - Requires specific knowledge to exploit

Comprehensive Solution

1. Install Validation Dependencies

pnpm add zod express-validator
pnpm add -D @types/express-validator

2. Create Validation Schemas with Zod

// server/lib/validation/schemas.ts
import { z } from 'zod'

// User validation schemas
export const userSchemas = {
  login: z.object({
    name: z.string()
      .min(1, 'Username is required')
      .max(255, 'Username too long')
      .regex(/^[a-zA-Z0-9_-]+$/, 'Username contains invalid characters'),
    password: z.string()
      .min(8, 'Password must be at least 8 characters')
      .max(128, 'Password too long')
  }),
  
  signup: z.object({
    name: z.string()
      .min(1, 'Username is required')
      .max(255, 'Username too long')
      .regex(/^[a-zA-Z0-9_-]+$/, 'Username contains invalid characters'),
    password: z.string()
      .min(8, 'Password must be at least 8 characters')
      .max(128, 'Password too long')
      .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must contain uppercase, lowercase, and number')
  })
}

// Post validation schemas
export const postSchemas = {
  create: z.object({
    title: z.string()
      .min(1, 'Title is required')
      .max(400, 'Title too long')
      .trim()
      .refine(val => val.length > 0, 'Title cannot be empty'),
    body: z.string()
      .min(1, 'Body is required')
      .max(50000, 'Body too long')
      .trim()
  }),
  
  update: z.object({
    id: z.string()
      .regex(/^\d+$/, 'Invalid post ID')
      .transform(val => parseInt(val, 10)),
    title: z.string()
      .min(1, 'Title is required')
      .max(400, 'Title too long')
      .trim(),
    body: z.string()
      .min(1, 'Body is required')
      .max(50000, 'Body too long')
      .trim()
  })
}

// Tweet validation schemas
export const tweetSchemas = {
  create: z.object({
    text: z.string()
      .min(1, 'Tweet text is required')
      .max(280, 'Tweet too long')
      .trim()
      .refine(val => val.length > 0, 'Tweet cannot be empty')
  })
}

// Stock validation schemas
export const stockSchemas = {
  create: z.object({
    pageTitle: z.string()
      .min(1, 'Page title is required')
      .max(1000, 'Page title too long')
      .trim(),
    url: z.string()
      .url('Invalid URL format')
      .max(2048, 'URL too long')
  })
}

3. Create Validation Middleware

// server/lib/validation/middleware.ts
import { Request, Response, NextFunction } from 'express'
import { ZodSchema, ZodError } from 'zod'
import Logger from '../Logger'

export const validateBody = (schema: ZodSchema) => {
  return (req: Request, res: Response, next: NextFunction) => {
    try {
      // Validate and transform the request body
      req.body = schema.parse(req.body)
      next()
    } catch (error) {
      if (error instanceof ZodError) {
        const validationErrors = error.errors.map(err => ({
          field: err.path.join('.'),
          message: err.message,
          code: err.code
        }))
        
        Logger.warn('Validation error:', {
          errors: validationErrors,
          body: req.body,
          ip: req.ip
        })
        
        return res.status(400).json({
          error: 'Validation failed',
          code: 'VALIDATION_ERROR',
          details: validationErrors
        })
      }
      
      Logger.error('Unexpected validation error:', error)
      return res.status(500).json({
        error: 'Internal validation error',
        code: 'INTERNAL_ERROR'
      })
    }
  }
}

export const validateQuery = (schema: ZodSchema) => {
  return (req: Request, res: Response, next: NextFunction) => {
    try {
      req.query = schema.parse(req.query)
      next()
    } catch (error) {
      if (error instanceof ZodError) {
        const validationErrors = error.errors.map(err => ({
          field: err.path.join('.'),
          message: err.message,
          code: err.code
        }))
        
        return res.status(400).json({
          error: 'Query validation failed',
          code: 'VALIDATION_ERROR',
          details: validationErrors
        })
      }
      
      next(error)
    }
  }
}

export const validateParams = (schema: ZodSchema) => {
  return (req: Request, res: Response, next: NextFunction) => {
    try {
      req.params = schema.parse(req.params)
      next()
    } catch (error) {
      if (error instanceof ZodError) {
        const validationErrors = error.errors.map(err => ({
          field: err.path.join('.'),
          message: err.message,
          code: err.code
        }))
        
        return res.status(400).json({
          error: 'Parameter validation failed',
          code: 'VALIDATION_ERROR',
          details: validationErrors
        })
      }
      
      next(error)
    }
  }
}

4. Sanitization Utilities

// server/lib/validation/sanitization.ts
import DOMPurify from 'isomorphic-dompurify'
import { z } from 'zod'

// HTML sanitization for rich content
export const sanitizeHtml = (html: string): string => {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
    ALLOWED_ATTR: ['href', 'title'],
    ALLOW_DATA_ATTR: false
  })
}

// SQL injection prevention
export const sanitizeSqlInput = (input: string): string => {
  return input
    .replace(/['"\\]/g, '') // Remove quotes and backslashes
    .replace(/--/g, '') // Remove SQL comments
    .replace(/;/g, '') // Remove semicolons
    .trim()
}

// XSS prevention
export const sanitizeXss = (input: string): string => {
  return input
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // Remove script tags
    .replace(/javascript:/gi, '') // Remove javascript: protocols
    .replace(/on\w+\s*=/gi, '') // Remove event handlers
    .trim()
}

// Custom Zod transformers for sanitization
export const sanitizeString = (maxLength?: number) => {
  return z.string()
    .transform(val => sanitizeXss(val))
    .refine(val => val.length > 0, 'String cannot be empty')
    .refine(val => !maxLength || val.length <= maxLength, `String too long (max ${maxLength})`)
}

export const sanitizeHtmlContent = (maxLength?: number) => {
  return z.string()
    .transform(val => sanitizeHtml(val))
    .refine(val => val.length > 0, 'Content cannot be empty')
    .refine(val => !maxLength || val.length <= maxLength, `Content too long (max ${maxLength})`)
}

5. Updated Route Implementations

// server/routes/post.ts
import { validateBody, validateParams } from '../lib/validation/middleware'
import { postSchemas } from '../lib/validation/schemas'
import { sanitizeHtmlContent } from '../lib/validation/sanitization'

// Create post with validation
router.post('/create', 
  isAuthorized, 
  validateBody(postSchemas.create),
  async (req: Request, res: Response) => {
    try {
      const { title, body } = req.body
      
      // Additional sanitization for rich content
      const sanitizedBody = sanitizeHtmlContent(50000).parse(body)
      
      const post = await prisma.post.create({
        data: {
          title: title.trim(),
          body: sanitizedBody,
        },
      })
      
      res.status(201).json({
        success: true,
        data: post
      })
    } catch (error: unknown) {
      Logger.error('Post creation error:', error)
      res.status(500).json({
        error: 'Failed to create post',
        code: 'POST_CREATION_ERROR'
      })
    }
  }
)

// Update post with validation
router.post('/update',
  isAuthorized,
  validateBody(postSchemas.update),
  async (req: Request, res: Response) => {
    try {
      const { id, title, body } = req.body
      
      // Verify post exists and user has permission
      const existingPost = await prisma.post.findFirst({
        where: { id }
      })
      
      if (!existingPost) {
        return res.status(404).json({
          error: 'Post not found',
          code: 'POST_NOT_FOUND'
        })
      }
      
      const sanitizedBody = sanitizeHtmlContent(50000).parse(body)
      
      await prisma.post.update({
        where: { id },
        data: {
          title: title.trim(),
          body: sanitizedBody,
        },
      })
      
      res.status(200).json({
        success: true,
        message: 'Post updated successfully'
      })
    } catch (error: unknown) {
      Logger.error('Post update error:', error)
      res.status(500).json({
        error: 'Failed to update post',
        code: 'POST_UPDATE_ERROR'
      })
    }
  }
)

// Get post with parameter validation
router.get('/post/:id',
  validateParams(z.object({ id: z.string().regex(/^\d+$/) })),
  async (req: Request, res: Response) => {
    try {
      const postId = parseInt(req.params.id, 10)
      
      const post = await prisma.post.findFirst({
        where: { id: postId },
      })
      
      if (!post) {
        return res.status(404).json({
          error: 'Post not found',
          code: 'POST_NOT_FOUND'
        })
      }
      
      res.status(200).json({
        success: true,
        data: post
      })
    } catch (error: unknown) {
      Logger.error('Post fetch error:', error)
      res.status(500).json({
        error: 'Failed to fetch post',
        code: 'POST_FETCH_ERROR'
      })
    }
  }
)

6. User Route Validation

// server/routes/user.ts
import { validateBody } from '../lib/validation/middleware'
import { userSchemas } from '../lib/validation/schemas'

// Signup with validation
router.post('/signup', 
  validateBody(userSchemas.signup),
  async (req: Request, res: Response) => {
    const { name, password } = req.body
    
    try {
      // Check if user already exists
      const existingUser = await prisma.user.findFirst({
        where: { name: name.toLowerCase() }
      })
      
      if (existingUser) {
        return res.status(409).json({
          error: 'Username already exists',
          code: 'USER_EXISTS'
        })
      }
      
      const salt = await bcrypt.genSalt(12) // Increased salt rounds
      const hash = await bcrypt.hash(password, salt)
      
      const user = await prisma.user.create({
        data: {
          name: name.toLowerCase(),
          password: hash,
        },
      })
      
      const token: JWTtoken = generateAccessToken(user)
      res.cookie('token', token, getCookieOptions(token))
      
      res.status(201).json({
        success: true,
        user: {
          id: user.id,
          name: user.name,
          createdAt: user.createdAt,
          updatedAt: user.updatedAt,
        }
      })
    } catch (error: unknown) {
      Logger.error('Signup error:', error)
      res.status(500).json({
        error: 'Failed to create account',
        code: 'SIGNUP_ERROR'
      })
    }
  }
)

7. Global Input Validation Middleware

// server/lib/validation/global.ts
import { Request, Response, NextFunction } from 'express'
import { z } from 'zod'

// Global request size limiter
export const requestSizeLimiter = (maxSize: number = 1024 * 1024) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const contentLength = parseInt(req.get('content-length') || '0', 10)
    
    if (contentLength > maxSize) {
      return res.status(413).json({
        error: 'Request too large',
        code: 'REQUEST_TOO_LARGE',
        maxSize: maxSize
      })
    }
    
    next()
  }
}

// Global content type validation
export const contentTypeValidator = (allowedTypes: string[] = ['application/json']) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const contentType = req.get('content-type')
    
    if (req.method !== 'GET' && req.method !== 'DELETE') {
      if (!contentType || !allowedTypes.some(type => contentType.includes(type))) {
        return res.status(415).json({
          error: 'Unsupported media type',
          code: 'UNSUPPORTED_MEDIA_TYPE',
          allowedTypes
        })
      }
    }
    
    next()
  }
}

// Global JSON parsing error handler
export const jsonErrorHandler = (error: any, req: Request, res: Response, next: NextFunction) => {
  if (error instanceof SyntaxError && 'body' in error) {
    return res.status(400).json({
      error: 'Invalid JSON format',
      code: 'INVALID_JSON'
    })
  }
  
  next(error)
}

Testing the Implementation

1. Validation Tests

// tests/validation.test.ts
import request from 'supertest'
import app from '../server/index'

describe('Input Validation', () => {
  describe('Post Creation', () => {
    it('should reject empty title', async () => {
      const response = await request(app)
        .post('/api/create')
        .send({ title: '', body: 'Valid body' })
        .expect(400)
      
      expect(response.body.code).toBe('VALIDATION_ERROR')
      expect(response.body.details[0].field).toBe('title')
    })
    
    it('should reject title too long', async () => {
      const longTitle = 'a'.repeat(401)
      const response = await request(app)
        .post('/api/create')
        .send({ title: longTitle, body: 'Valid body' })
        .expect(400)
      
      expect(response.body.code).toBe('VALIDATION_ERROR')
    })
    
    it('should reject malicious HTML in body', async () => {
      const maliciousBody = '<script>alert("xss")</script>Valid content'
      const response = await request(app)
        .post('/api/create')
        .send({ title: 'Valid title', body: maliciousBody })
        .expect(201)
      
      // Should sanitize the content
      expect(response.body.data.body).not.toContain('<script>')
    })
  })
  
  describe('User Signup', () => {
    it('should reject weak password', async () => {
      const response = await request(app)
        .post('/api/signup')
        .send({ name: 'testuser', password: 'weak' })
        .expect(400)
      
      expect(response.body.code).toBe('VALIDATION_ERROR')
    })
    
    it('should reject invalid username characters', async () => {
      const response = await request(app)
        .post('/api/signup')
        .send({ name: 'test@user!', password: 'ValidPass123' })
        .expect(400)
      
      expect(response.body.code).toBe('VALIDATION_ERROR')
    })
  })
})

2. Security Testing

# Test for SQL injection
curl -X POST http://localhost:3000/api/create \
  -H "Content-Type: application/json" \
  -d '{"title": "test", "body": "'; DROP TABLE posts; --"}'

# Test for XSS
curl -X POST http://localhost:3000/api/create \
  -H "Content-Type: application/json" \
  -d '{"title": "test", "body": "<script>alert(\"xss\")</script>"}'

# Test for oversized payload
curl -X POST http://localhost:3000/api/create \
  -H "Content-Type: application/json" \
  -d "{\"title\": \"test\", \"body\": \"$(printf 'a%.0s' {1..1000000})\"}"

Migration Strategy

Phase 1: Basic Validation

  1. Add Zod schemas for all endpoints
  2. Implement validation middleware
  3. Add basic sanitization

Phase 2: Enhanced Security

  1. Add HTML sanitization
  2. Implement request size limiting
  3. Add content type validation

Phase 3: Advanced Protection

  1. Add rate limiting per endpoint
  2. Implement input monitoring
  3. Add anomaly detection

Monitoring and Alerting

1. Validation Error Monitoring

// Track validation errors
export const trackValidationError = (error: ZodError, req: Request) => {
  Logger.warn('Validation error:', {
    errors: error.errors,
    body: req.body,
    ip: req.ip,
    userAgent: req.get('user-agent')
  })
  
  // Send to monitoring service
  if (process.env.NODE_ENV === 'production') {
    // sendToMonitoringService('validation_error', { ... })
  }
}

2. Suspicious Input Detection

// Detect potentially malicious input
export const detectSuspiciousInput = (input: string): boolean => {
  const suspiciousPatterns = [
    /<script/i,
    /javascript:/i,
    /on\w+\s*=/i,
    /union\s+select/i,
    /drop\s+table/i,
    /insert\s+into/i
  ]
  
  return suspiciousPatterns.some(pattern => pattern.test(input))
}

References


Priority: 🟡 Medium
Estimated Effort: 8-12 hours
Risk if not fixed: Medium - Potential injection vulnerabilities

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions