Inclusion of Sensitive Information in Test Code

Description

Inclusion of Sensitive Information in Test Code is a vulnerability where test applications or test code contains sensitive information that becomes accessible to unauthorized parties. Developers often include real credentials, API keys, configuration details, and other sensitive data in test code without anticipating that this code might be deployed to production, included in repositories, or otherwise made accessible. Test code commonly contains administrative functions, hardcoded usernames and passwords, session identifiers, database connection strings, and detailed system configuration information that can be exploited if exposed.

Risk

Test code with sensitive information creates multiple security risks. Real credentials or API keys in test files may be committed to version control and exposed through repository leaks. Test code accidentally deployed to production provides attackers with administrative access or debugging capabilities. Unit tests may contain valid database credentials that provide unauthorized data access. Integration tests might expose internal API endpoints or authentication bypass mechanisms. Test fixtures and sample data may include real personal information. The informal nature of test code often leads to weaker security practices, making it a rich source of sensitive information for attackers.

Solution

Remove all test code, test pages, and debugging functionality before deploying applications to production. Never use real credentials or production secrets in test code - use mock data, test credentials, or environment-specific configuration. Implement automated checks in CI/CD pipelines to detect test code or test artifacts in production deployments. Use separate test databases and test API keys that have no access to production resources. Store test configuration separately from production configuration. Review test code for sensitive information before committing to repositories. Implement .gitignore rules to exclude test credentials.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Adversaries can access sensitive information embedded in test code, including credentials, API keys, personal data, and system configuration details.

Example Code

Vulnerable Code

// Vulnerable: Test class with hardcoded credentials
package com.example.tests;

import org.junit.Test;
import static org.junit.Assert.*;

public class VulnerableDatabaseTest {

    // Vulnerable: Real production credentials in test!
    private static final String DB_HOST = "prod-db.company.com";
    private static final String DB_USER = "admin";
    private static final String DB_PASSWORD = "Production_P@ssw0rd_2024!";

    // Vulnerable: Real API keys
    private static final String API_KEY = "sk_live_51ABC123XYZ";
    private static final String SECRET_KEY = "whsec_secret123456";

    @Test
    public void testDatabaseConnection() {
        // Test uses real production credentials
        Connection conn = DriverManager.getConnection(
            "jdbc:mysql://" + DB_HOST + "/production",
            DB_USER,
            DB_PASSWORD
        );
        assertTrue(conn.isValid(5));
    }

    @Test
    public void testApiIntegration() {
        // Test uses real production API key
        ApiClient client = new ApiClient(API_KEY, SECRET_KEY);
        Response response = client.makeRequest("/live/endpoint");
        assertEquals(200, response.getStatus());
    }
}
# Vulnerable: Test file with sensitive information
# tests/test_integration.py

import unittest
import requests

# Vulnerable: Real AWS credentials in test file
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
AWS_BUCKET = "production-user-data"

# Vulnerable: Real database credentials
DATABASE_URL = "postgresql://admin:[email protected]:5432/users"

# Vulnerable: Admin test accounts with real passwords
TEST_ADMIN = {
    "username": "[email protected]",
    "password": "AdminPass123!",  # Real admin password!
    "role": "superuser"
}

class IntegrationTests(unittest.TestCase):

    def test_admin_login(self):
        # Vulnerable: Uses real admin credentials
        response = requests.post(
            "https://app.company.com/login",
            json=TEST_ADMIN
        )
        self.assertEqual(response.status_code, 200)

    def test_s3_access(self):
        # Vulnerable: Uses real AWS credentials
        import boto3
        s3 = boto3.client('s3',
            aws_access_key_id=AWS_ACCESS_KEY,
            aws_secret_access_key=AWS_SECRET_KEY
        )
        # Accesses real production bucket!
        objects = s3.list_objects(Bucket=AWS_BUCKET)
// Vulnerable: Test configuration with secrets
// tests/config.test.js

const testConfig = {
    // Vulnerable: Production API keys
    stripe: {
        publishableKey: 'pk_live_51ABC123',
        secretKey: 'sk_live_51ABC123XYZ789',
        webhookSecret: 'whsec_realwebhooksecret'
    },

    // Vulnerable: Real OAuth credentials
    oauth: {
        clientId: 'real-production-client-id',
        clientSecret: 'real-production-client-secret',
        redirectUri: 'https://app.company.com/callback'
    },

    // Vulnerable: Real admin credentials
    adminUser: {
        email: '[email protected]',
        password: 'Admin123!',
        totpSecret: 'JBSWY3DPEHPK3PXP'  // TOTP seed exposed!
    },

    // Vulnerable: Internal API endpoints
    internalApis: {
        userService: 'http://internal-user-api:8080',
        paymentService: 'http://internal-payment-api:8080',
        adminPanel: 'https://admin.internal.company.com'
    }
};

module.exports = testConfig;
<?php
// Vulnerable: Test page deployed to production
// /var/www/html/test.php (should not exist in production!)

// Vulnerable: Debug mode enabling
error_reporting(E_ALL);
ini_set('display_errors', 1);

// Vulnerable: Hardcoded test credentials
$test_admin_user = 'admin';
$test_admin_pass = 'admin123';  // Weak test password that works!

// Vulnerable: Database test credentials
$test_db_config = [
    'host' => 'localhost',
    'user' => 'root',
    'pass' => 'root_password_123',
    'database' => 'production'
];

// Vulnerable: Test function exposing system info
function debug_system_info() {
    phpinfo();  // Exposes all PHP configuration
    echo "<pre>";
    print_r($_SERVER);  // Server information
    print_r($_ENV);     // Environment variables
    echo "</pre>";
}

// Vulnerable: Authentication bypass for testing
if ($_GET['test_mode'] === 'enabled') {
    $_SESSION['authenticated'] = true;
    $_SESSION['role'] = 'admin';
}
?>

Fixed Code

// Fixed: Test class using mock data and test credentials
package com.example.tests;

import org.junit.Test;
import org.junit.BeforeClass;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class SecureDatabaseTest {

    // Fixed: Use environment variables for test credentials
    private static String testDbHost;
    private static String testDbUser;
    private static String testDbPassword;

    @BeforeClass
    public static void setup() {
        // Fixed: Load from test-specific environment
        testDbHost = System.getenv("TEST_DB_HOST");
        testDbUser = System.getenv("TEST_DB_USER");
        testDbPassword = System.getenv("TEST_DB_PASSWORD");

        // Fixed: Validate we're not using production
        if (testDbHost != null && testDbHost.contains("prod")) {
            throw new IllegalStateException(
                "Tests must not run against production!"
            );
        }
    }

    @Test
    public void testDatabaseConnection() {
        // Fixed: Use test database, not production
        // Or better, use an in-memory database for unit tests
        Connection conn = DriverManager.getConnection(
            "jdbc:h2:mem:testdb",
            "sa",
            ""
        );
        assertTrue(conn.isValid(5));
    }

    @Test
    public void testApiIntegration() {
        // Fixed: Mock the API client for unit tests
        ApiClient mockClient = mock(ApiClient.class);
        when(mockClient.makeRequest("/endpoint"))
            .thenReturn(new Response(200, "OK"));

        Response response = mockClient.makeRequest("/endpoint");
        assertEquals(200, response.getStatus());
    }
}
# Fixed: Test file using proper test configuration
# tests/test_integration.py

import unittest
import os
from unittest.mock import patch, MagicMock

# Fixed: Load credentials from environment, never hardcode
def get_test_config():
    """Get test configuration from environment."""
    env = os.environ.get('TEST_ENV', 'test')

    # Fixed: Ensure we're not in production
    if env == 'production':
        raise RuntimeError("Tests cannot run in production!")

    return {
        'db_url': os.environ.get('TEST_DATABASE_URL'),
        'api_key': os.environ.get('TEST_API_KEY'),
    }

# Fixed: Use test fixtures with fake data
TEST_USER = {
    "username": "[email protected]",  # Fake email
    "password": "TestPassword123",        # Test-only password
    "role": "user"
}

class IntegrationTests(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        # Fixed: Verify test environment
        if os.environ.get('ENVIRONMENT') == 'production':
            raise RuntimeError("Integration tests cannot run in production!")

        cls.config = get_test_config()

    @patch('requests.post')
    def test_user_login(self, mock_post):
        # Fixed: Mock external calls
        mock_post.return_value = MagicMock(status_code=200)

        response = requests.post(
            "https://test.example.com/login",
            json=TEST_USER
        )
        self.assertEqual(response.status_code, 200)

    @patch('boto3.client')
    def test_s3_access(self, mock_boto):
        # Fixed: Mock AWS client
        mock_s3 = MagicMock()
        mock_boto.return_value = mock_s3
        mock_s3.list_objects.return_value = {'Contents': []}

        import boto3
        s3 = boto3.client('s3')
        objects = s3.list_objects(Bucket='test-bucket')

        # Never touches real AWS
        self.assertIsNotNone(objects)
// Fixed: Test configuration using environment variables
// tests/config.test.js

// Fixed: Load from environment, with test-specific defaults
const testConfig = {
    stripe: {
        // Fixed: Test mode keys only
        publishableKey: process.env.TEST_STRIPE_PK || 'pk_test_placeholder',
        secretKey: process.env.TEST_STRIPE_SK || 'sk_test_placeholder',
        webhookSecret: process.env.TEST_STRIPE_WEBHOOK || 'whsec_test'
    },

    // Fixed: Mock OAuth for tests
    oauth: {
        clientId: 'test-client-id',
        clientSecret: 'test-client-secret',
        redirectUri: 'http://localhost:3000/callback'
    },

    // Fixed: Test user with fake data
    testUser: {
        email: '[email protected]',  // Fake domain
        password: 'TestOnly123',    // Test-only password
        // No TOTP secrets in code
    },

    // Fixed: Use localhost or mock URLs
    apis: {
        userService: 'http://localhost:8080',
        paymentService: 'http://localhost:8081',
    }
};

// Fixed: Validation to prevent production use
if (process.env.NODE_ENV === 'production') {
    throw new Error('Test config must not be used in production!');
}

module.exports = testConfig;
<?php
// Fixed: No test pages in production deployment

// Fixed: Test endpoints in separate test directory not deployed
// tests/helpers/test_utils.php (NOT in webroot)

/**
 * Test utilities - NEVER DEPLOY TO PRODUCTION
 */

// Fixed: Environment check at the top of all test files
if (getenv('ENVIRONMENT') === 'production') {
    die('Test files cannot run in production!');
}

// Fixed: Test credentials from environment only
function getTestCredentials() {
    return [
        'user' => getenv('TEST_USER') ?: 'testuser',
        'pass' => getenv('TEST_PASS') ?: 'testpass',
    ];
}

// Fixed: Test database uses separate, empty database
function getTestDatabaseConfig() {
    $config = [
        'host' => getenv('TEST_DB_HOST') ?: 'localhost',
        'user' => getenv('TEST_DB_USER') ?: 'test_user',
        'pass' => getenv('TEST_DB_PASS') ?: '',
        'database' => getenv('TEST_DB_NAME') ?: 'test_db',
    ];

    // Fixed: Prevent accidental production use
    if (strpos($config['database'], 'prod') !== false) {
        throw new Exception('Cannot use production database for tests!');
    }

    return $config;
}
# Fixed: CI/CD pipeline preventing test code deployment
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      # Fixed: Remove test files before deployment
      - name: Remove test code
        run: |
          rm -rf tests/
          rm -rf test/
          rm -rf spec/
          rm -rf __tests__/
          rm -f **/test_*.py
          rm -f **/*_test.py
          rm -f **/*.test.js
          rm -f **/*.spec.js
          rm -f **/Test*.java
          rm -f **/*Test.java

      # Fixed: Scan for test patterns in remaining code
      - name: Check for test code
        run: |
          if grep -rn "test_password\|TEST_API_KEY\|pk_test_\|sk_test_" --include="*.py" --include="*.js" --include="*.php" --include="*.java" .; then
            echo "ERROR: Test credentials found in deployment!"
            exit 1
          fi

      # Fixed: Check for debug code
      - name: Check for debug code
        run: |
          if grep -rn "phpinfo()\|var_dump(\|console.log(\|debugger;" --include="*.php" --include="*.js" .; then
            echo "WARNING: Debug code found - review before deploying"
          fi

      - name: Deploy
        run: ./deploy.sh

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, the vulnerability pattern is extremely common:

  • Exposed API keys in GitHub repositories
  • Test pages left in production deployments
  • Credentials in test files committed to public repos

References

  1. MITRE Corporation. "CWE-531: Inclusion of Sensitive Information in Test Code." https://cwe.mitre.org/data/definitions/531.html
  2. OWASP. "Testing Guide - Test Code Review."
  3. GitHub. "Secret Scanning Documentation."