Skip to content

CWE-99: Resource Injection

Overview

Resource Injection occurs when untrusted input is used to select or reference system resources (e.g., files, ports, class names) without validation, allowing attackers to manipulate which resources are used by the application. Untrusted input can originate from HTTP requests, external APIs, databases, files, network requests, or any source outside the application's control.

OWASP Classification

A05:2025 - Injection

Risk

High: Attackers may redirect application behavior, access unauthorized resources, or cause denial of service by injecting malicious resource identifiers.

Remediation Steps

Core principle: Never let untrusted input select resources by name/path; canonicalize and map identifiers to allowlisted resources.

Trace the Data Path

Analyze how untrusted data influences resource selection:

Review scan results:

  • Source: Identify where untrusted data enters (HTTP requests, external files, databases, network requests)
  • Resource Selection: Trace how data determines which resource is accessed
  • Sink: Locate resource access (file paths, ports, class names, URLs, database connections)
  • Missing Validation: Identify lack of allowlist or validation

Find resource injection vulnerabilities proactively (good developer habits):

During development:

  • Map all dynamic resource selection: Identify every place user input determines which resource to use (file names, template names, class names, port numbers, database names)
  • Question resource parameters: Any time you see user input in resource selection, ask "Could this be malicious?"
  • Understand resource boundaries: Know what resources exist in your system and what attackers might target
  • Prefer static resource selection: Hardcode resource choices when possible; use user input only to select from predefined options

Code review checklist:

  • Check for dynamic file loading: Does user input determine which file/template to load?
  • Review class instantiation: Is user input used in class names, module imports, or reflection?
  • Audit network operations: Can users specify ports, hosts, or protocol configurations?
  • Examine database selection: Does user input choose which database, table, or connection pool?
  • Look for indirect injection: Even if code uses IDs, verify the ID-to-resource mapping is secure

Search your codebase for vulnerable patterns:

  • File operations with user input: open(user_input), readFile(req.params.file), include($_GET['page'])
  • Dynamic class loading: Class.forName(user_input), __import__(user_input), require(user_input)
  • Port/socket operations: ServerSocket(user_port), bind(user_port), listen(user_port)
  • Template loading: render_template(user_template), getTemplate(req.query.template)
  • Database selection: use ${user_db}, connect(user_dbname), switching connections based on input
  • URL construction: redirect(user_url), fetch(user_endpoint)

Use development tools:

  • Static analysis: Configure tools to flag dynamic resource loading (Semgrep rules, CodeQL queries)
  • IDE warnings: Set up inspections for reflection, dynamic imports, file operations with variables
  • Dependency scanning: Check if frameworks/libraries do dynamic loading based on request parameters
  • Code review automation: Add pre-commit checks for common resource injection patterns

Architectural best practices:

  • Resource mapping layer: Create central mapping from user IDs to actual resources
  • Enumeration over strings: Use enums for resource selection instead of string matching
  • Configuration-based resources: Define available resources in config files, not derived from user input
  • Principle of least privilege: Only make necessary resources available to the application
  • Immutable resource sets: Define resource lists at application startup, don't modify at runtime based on user input

Validate Resource Identifiers Against Allowlists (Primary Defense)

Only allow selection from predefined resources:

  • Define allowed resources: Maintain explicit list of permitted identifiers
  • Validate against allowlist: Reject anything not explicitly allowed
  • Use exact matching: No pattern matching or partial matches
  • Example (ports):
List<Integer> allowedPorts = Arrays.asList(8080, 8443, 9000);
int port = Integer.parseInt(request.getParameter("port"));
if (allowedPorts.contains(port)) {
    ServerSocket socket = new ServerSocket(port);
} else {
    throw new IllegalArgumentException("Invalid port");
}

Why this works: Users can only select from pre-approved resources.

Use Indirect References

Map untrusted input to internal identifiers:

  • Map IDs to resources: User provides ID, system maps to actual resource
  • Never expose internal paths: Keep file paths, class names internal
  • Example (file selection):
Map<String, String> fileMap = Map.of(
    "report1", "/var/reports/monthly.pdf",
    "report2", "/var/reports/annual.pdf"
);
String fileId = request.getParameter("report");
String filePath = fileMap.get(fileId);
if (filePath != null) {
    readFile(filePath);
}

Restrict Resource Access Scope

Limit available resources:

  • Least privilege: Only make required resources accessible
  • Restrict by type: Limit to specific resource categories
  • Boundary checking: Ensure resources stay within allowed boundaries
  • Type validation: Ensure resource type matches expected type

Monitor and Log Resource Usage

Enable detection:

  • Log all resource selection: Record requested and actual resources used
  • Alert on failures: Track rejected or suspicious resource access
  • Monitor patterns: Detect scanning or enumeration attempts
  • Review logs: Identify unauthorized access attempts

Test with Malicious Resource Identifiers

Verify your fixes:

  • Test with unauthorized resources: invalid ports, restricted files
  • Test with special characters: ../, null bytes, URL encoding
  • Test with extreme values: negative numbers, very large numbers
  • Test resource enumeration: sequential IDs, common names
  • Ensure legitimate resource access still works
  • Re-scan with security scanner

Common Vulnerable Patterns

File/Template Resource Injection (Python)

from flask import Flask, request, render_template

@app.route('/page')
def show_page():
    # User controls which template is loaded
    template_name = request.args.get('template')

    # Attack: template=../../../../etc/passwd
    # Attack: template=../../../app/config.py
    # Result: Reads arbitrary files or exposes source code
    return render_template(template_name)

Class Name Resource Injection (Java)

@GetMapping("/handler")
public Response processRequest(@RequestParam String handlerClass) {
    // User controls which class is instantiated
    try {
        Class<?> clazz = Class.forName(handlerClass);
        RequestHandler handler = (RequestHandler) clazz.newInstance();

        // Attack: handlerClass=java.lang.Runtime
        // Attack: handlerClass=com.attacker.MaliciousClass
        // Result: Arbitrary class instantiation, potential RCE

        return handler.process(request);
    } catch (Exception e) {
        return Response.serverError().build();
    }
}

Port Binding Resource Injection (Node.js)

const http = require('http');
const express = require('express');

app.post('/start-service', (req, res) => {
    const port = req.body.port;

    // User controls which port to bind
    // Attack: port=22 (bind to SSH port)
    // Attack: port=80 (bind to HTTP, requires root)
    // Attack: port=3306 (MySQL port, intercept DB connections)

    const server = http.createServer(app);
    server.listen(port, () => {
        res.send(`Service started on port ${port}`);
    });
});

Database Selection Resource Injection (PHP)

<?php
// User controls which database to query
$dbname = $_GET['database'];

$conn = new mysqli($host, $user, $pass, $dbname);

// Attack: database=information_schema (access metadata)
// Attack: database=mysql (access user tables)
// Attack: database=other_customer_db (access other tenant's data)

$result = $conn->query("SELECT * FROM products");

URL/Redirect Resource Injection (C#)

[HttpGet("redirect")]
public IActionResult RedirectUser([FromQuery] string url)
{
    // User controls redirect destination
    // Attack: url=https://evil.com/phishing
    // Attack: url=javascript:alert(document.cookie)
    // Attack: url=//attacker.com (protocol-relative URL)
    // Result: Open redirect, phishing, XSS

    return Redirect(url);
}

Module/Plugin Resource Injection (Python)

import importlib

@app.route('/load-plugin')
def load_plugin():
    plugin_name = request.args.get('plugin')

    # User controls which module to import
    # Attack: plugin=os (import os module)
    # Attack: plugin=subprocess (import subprocess)
    # Attack: plugin=__builtins__ (access built-ins)
    # Result: Arbitrary code execution

    module = importlib.import_module(plugin_name)
    result = module.process()
    return result

Configuration File Resource Injection (Node.js)

const fs = require('fs');
const path = require('path');

app.get('/config', (req, res) => {
    const configName = req.query.config;

    // User controls which config file to load
    const configPath = path.join('/app/config/', configName);

    // Attack: config=../../../etc/passwd
    // Attack: config=../app.js (read source code)
    // Attack: config=../../.env (read secrets)

    const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
    res.json(config);
});

Secure Patterns

Template Allowlist Mapping (Python)

from flask import Flask, request, render_template, abort

# Define allowed templates
ALLOWED_TEMPLATES = {
    'home': 'pages/home.html',
    'about': 'pages/about.html',
    'contact': 'pages/contact.html',
    'products': 'pages/products.html'
}

@app.route('/page')
def show_page():
    template_id = request.args.get('template', 'home')

    # Validate against allowlist
    template_path = ALLOWED_TEMPLATES.get(template_id)

    if template_path is None:
        abort(404, "Page not found")

    # Use mapped template path
    return render_template(template_path)

Why this works:

  • Indirect reference mapping: User provides a key (template_id), not the actual file path, preventing path traversal attacks
  • Allowlist validation: Only predefined template keys (home, about, etc.) are accepted; anything else is rejected
  • No user input in file paths: The actual file paths (pages/home.html) are hardcoded in the mapping, eliminating injection
  • Default value: Falls back to home if no template specified, preventing null/empty exploits
  • Clear error handling: Returns 404 for invalid templates instead of exposing error details

Class Allowlist with Type Safety (Java)

import java.util.Map;
import java.util.HashMap;

private static final Map<String, Class<? extends RequestHandler>> HANDLER_CLASSES = 
    Map.of(
        "basic", BasicRequestHandler.class,
        "advanced", AdvancedRequestHandler.class,
        "special", SpecialRequestHandler.class
    );

@GetMapping("/handler")
public Response processRequest(@RequestParam String handlerType) {
    // Validate against allowlist
    Class<? extends RequestHandler> handlerClass = HANDLER_CLASSES.get(handlerType);

    if (handlerClass == null) {
        throw new BadRequestException("Invalid handler type");
    }

    try {
        // Instantiate only allowed classes
        RequestHandler handler = handlerClass.getDeclaredConstructor().newInstance();
        return handler.process(request);
    } catch (Exception e) {
        throw new ServerException("Handler initialization failed");
    }
}

Why this works:

  • Class reference allowlist: Maps string keys to actual Class objects, preventing arbitrary class loading via Class.forName()
  • Type-safe mapping: Class<? extends RequestHandler> ensures only classes implementing RequestHandler can be registered
  • No reflection on user input: User input selects from predefined classes, not used directly in reflection calls
  • Compile-time safety: Invalid class references would cause compilation errors, not runtime exploits
  • Immutable mapping: Map.of() creates immutable map, preventing runtime modification of allowed classes

Port Validation with Range Checking (Node.js)

const http = require('http');
const express = require('express');

// Define allowed ports for services
const ALLOWED_PORTS = [8080, 8081, 8082, 9000, 9001];

app.post('/start-service', (req, res) => {
    const portInput = req.body.port;

    // Validate port is a number
    const port = parseInt(portInput, 10);
    if (isNaN(port)) {
        return res.status(400).send('Port must be a number');
    }

    // Validate against allowlist
    if (!ALLOWED_PORTS.includes(port)) {
        return res.status(400).send('Port not allowed. Allowed ports: ' + 
            ALLOWED_PORTS.join(', '));
    }

    // Check port is in valid range
    if (port < 1024 || port > 65535) {
        return res.status(400).send('Port must be between 1024 and 65535');
    }

    const server = http.createServer(app);
    server.listen(port, () => {
        res.send(`Service started on port ${port}`);
    });
});

Why this works:

  • Type validation: Parses and validates input is a number, preventing string-based exploits
  • Port allowlist: Only specific ports (8080, 8081, 8082, 9000, 9001) can be used, blocking privileged ports like 22, 80, 443
  • Range validation: Additional check ensures port is in unprivileged range (>1024), preventing binding to system services
  • Multiple validation layers: Combines type checking, allowlist, and range validation for defense in depth
  • Clear error messages: Provides specific feedback about what ports are allowed

Multi-Tenant Database Mapping with Authorization (PHP)

<?php
// Define allowed databases (multi-tenant scenario)
$allowed_databases = [
    'tenant_a' => 'customer_001_db',
    'tenant_b' => 'customer_002_db',
    'tenant_c' => 'customer_003_db'
];

$tenant_id = $_GET['tenant'] ?? '';

// Validate tenant ID against allowlist
if (!isset($allowed_databases[$tenant_id])) {
    http_response_code(403);
    die('Invalid tenant');
}

// Verify user is authorized for this tenant
if (!$current_user->hasAccessTo($tenant_id)) {
    http_response_code(403);
    die('Unauthorized');
}

// Use mapped database name
$dbname = $allowed_databases[$tenant_id];

$conn = new mysqli($host, $user, $pass, $dbname);

if ($conn->connect_error) {
    http_response_code(500);
    die('Connection failed');
}

$result = $conn->query("SELECT * FROM products");

Why this works:

  • Tenant ID mapping: Maps friendly tenant IDs to actual database names, preventing direct database name injection
  • Allowlist validation: Only predefined tenant IDs are accepted, blocking access to information_schema, mysql, or other tenants
  • Authorization check: Even with valid tenant ID, verifies current user has permission to access that tenant's data
  • Database name isolation: Actual database names (customer_001_db) are internal; users never provide them directly
  • Multi-layer security: Combines resource mapping with access control for complete protection

Redirect Destination Allowlist with Domain Validation (C#)

using System;
using System.Collections.Generic;

private static readonly Dictionary<string, string> ALLOWED_REDIRECTS = 
    new Dictionary<string, string>
    {
        { "home", "/" },
        { "profile", "/user/profile" },
        { "settings", "/user/settings" },
        { "logout", "/auth/logout" },
        { "help", "https://help.example.com" }
    };

[HttpGet("redirect")]
public IActionResult RedirectUser([FromQuery] string destination)
{
    if (string.IsNullOrWhiteSpace(destination))
    {
        return BadRequest("Destination required");
    }

    // Validate against allowlist
    if (!ALLOWED_REDIRECTS.TryGetValue(destination, out string url))
    {
        return BadRequest("Invalid destination");
    }

    // Additional validation for external URLs
    if (url.StartsWith("http"))
    {
        Uri uri = new Uri(url);

        // Only allow specific external domains
        var allowedHosts = new[] { "help.example.com", "support.example.com" };
        if (!allowedHosts.Contains(uri.Host))
        {
            return BadRequest("External domain not allowed");
        }
    }

    return Redirect(url);
}

Why this works:

  • Destination key mapping: User provides a key (home, profile), not the actual URL, preventing open redirect attacks
  • URL allowlist: All redirect destinations are predefined, blocking redirects to phishing sites or javascript: URLs
  • External domain validation: For HTTPS URLs, validates domain against allowlist, preventing redirects to attacker domains
  • Protocol-relative URL prevention: No URLs starting with // can be injected since all URLs are hardcoded
  • Input validation: Checks for null/empty input before processing

Plugin Module Allowlist with Interface Verification (Python)

import importlib
from typing import Dict, Any

# Define allowed plugins with their module paths
ALLOWED_PLUGINS: Dict[str, str] = {
    'stats': 'app.plugins.statistics',
    'export': 'app.plugins.export',
    'report': 'app.plugins.reporting'
}

# Optional: Validate plugin signatures/checksums at startup
def validate_plugin(module_path: str) -> bool:
    """Verify plugin is authentic and not tampered with"""
    # Implementation would verify plugin integrity
    return True

@app.route('/load-plugin')
def load_plugin():
    plugin_id = request.args.get('plugin', '')

    # Validate against allowlist
    module_path = ALLOWED_PLUGINS.get(plugin_id)

    if module_path is None:
        abort(404, "Plugin not found")

    # Additional security check
    if not validate_plugin(module_path):
        abort(500, "Plugin validation failed")

    try:
        # Import only allowed module
        module = importlib.import_module(module_path)

        # Verify module has expected interface
        if not hasattr(module, 'process'):
            abort(500, "Invalid plugin interface")

        result = module.process()
        return result
    except ImportError:
        abort(500, "Plugin load failed")

Why this works:

  • Module path allowlist: Maps plugin IDs to specific module paths, preventing import of dangerous modules like os, subprocess, or __builtins__
  • Namespace isolation: All allowed plugins are under app.plugins.*, preventing access to arbitrary Python modules
  • Interface verification: Checks that loaded module has expected process() method before calling, preventing errors from malformed modules
  • Integrity validation: Optional signature/checksum verification ensures plugins haven't been tampered with
  • No direct import: User input never passed directly to import_module(), eliminating arbitrary code execution

Configuration File Allowlist with Sensitive Field Filtering (Node.js)

const fs = require('fs');
const path = require('path');

// Define allowed configuration files
const ALLOWED_CONFIGS = {
    'app': '/app/config/application.json',
    'db': '/app/config/database.json',
    'cache': '/app/config/cache.json'
};

app.get('/config', (req, res) => {
    const configId = req.query.config || 'app';

    // Validate against allowlist
    const configPath = ALLOWED_CONFIGS[configId];

    if (!configPath) {
        return res.status(404).send('Configuration not found');
    }

    try {
        // Verify file exists and is readable
        if (!fs.existsSync(configPath)) {
            return res.status(404).send('Configuration file missing');
        }

        const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));

        // Filter sensitive fields before returning
        const sanitized = {
            name: config.name,
            version: config.version,
            features: config.features
            // Don't return: passwords, secrets, API keys
        };

        res.json(sanitized);
    } catch (error) {
        res.status(500).send('Error loading configuration');
    }
});

Why this works:

  • Configuration ID mapping: User provides config ID, not file path, preventing path traversal like ../../../etc/passwd
  • Absolute path allowlist: All config file paths are absolute and hardcoded, preventing relative path manipulation
  • File existence check: Validates file exists before reading, preventing errors and information disclosure
  • Sensitive field filtering: Only returns non-sensitive fields (name, version, features), stripping passwords and API keys
  • Error handling: Generic error messages prevent leaking file system details or configuration structure

Additional Resources