Testing & TDD

Security Testing

30 min Lesson 23 of 35

Security Testing

Security testing identifies vulnerabilities before attackers exploit them. In this lesson, we'll explore vulnerability scanning, penetration testing basics, OWASP testing guidelines, dependency audits, and practical security testing techniques to protect your applications.

Why Security Testing Matters

Security testing helps you:

  • Identify vulnerabilities before production deployment
  • Protect user data and prevent breaches
  • Comply with security standards (PCI-DSS, GDPR, HIPAA)
  • Prevent financial losses from security incidents
  • Maintain customer trust and brand reputation
  • Detect outdated dependencies with known vulnerabilities
  • Validate security controls and authentication mechanisms
Important: Security testing is not a one-time activity. Regular testing, continuous monitoring, and staying updated with emerging threats are essential for maintaining application security.

OWASP Top 10 Vulnerabilities

The OWASP Top 10 represents the most critical web application security risks:

  • A01: Broken Access Control - Unauthorized access to resources
  • A02: Cryptographic Failures - Exposure of sensitive data
  • A03: Injection - SQL, command, LDAP injection attacks
  • A04: Insecure Design - Fundamental flaws in architecture
  • A05: Security Misconfiguration - Default configs, verbose errors
  • A06: Vulnerable Components - Outdated libraries with known CVEs
  • A07: Authentication Failures - Weak password policies, session issues
  • A08: Software & Data Integrity - Insecure CI/CD pipelines
  • A09: Logging & Monitoring Failures - Insufficient audit trails
  • A10: Server-Side Request Forgery (SSRF) - Fetching remote resources

Testing for SQL Injection

SQL injection is one of the most dangerous vulnerabilities. Test your application for proper input sanitization:

<?php namespace Tests\Security; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class SqlInjectionTest extends TestCase { use RefreshDatabase; public function test_login_prevents_sql_injection() { User::factory()->create([ 'email' => 'admin@example.com', 'password' => bcrypt('password123'), ]); // Attempt SQL injection $response = $this->post('/login', [ 'email' => "admin@example.com' OR '1'='1", 'password' => "anything' OR '1'='1", ]); // Should NOT authenticate $response->assertRedirect('/login'); $response->assertSessionHasErrors(); $this->assertGuest(); } public function test_search_prevents_sql_injection() { // Create test data Product::factory()->count(10)->create(); // Attempt SQL injection in search $maliciousQueries = [ "' OR 1=1--", "' UNION SELECT * FROM users--", "'; DROP TABLE products;--", "1' AND 1=(SELECT COUNT(*) FROM users)--", ]; foreach ($maliciousQueries as $query) { $response = $this->getJson("/api/search?q=" . urlencode($query)); // Should return safe results or empty $response->assertOk(); // Should NOT expose database structure $this->assertStringNotContainsString('users', $response->content()); $this->assertStringNotContainsString('DROP TABLE', $response->content()); } } public function test_parameterized_queries_are_used() { // Verify that raw queries are not used $searchTerm = "test' OR '1'='1"; DB::enableQueryLog(); Product::where('name', 'like', "%{$searchTerm}%")->get(); $queries = DB::getQueryLog(); // Ensure query uses parameter binding $this->assertNotEmpty($queries); $this->assertStringContainsString('?', $queries[0]['query']); $this->assertContains($searchTerm, $queries[0]['bindings']); } }

Testing for Cross-Site Scripting (XSS)

XSS vulnerabilities allow attackers to inject malicious scripts into web pages:

<?php namespace Tests\Security; use Tests\TestCase; class XssProtectionTest extends TestCase { public function test_user_input_is_escaped_in_views() { $user = User::factory()->create([ 'name' => '<script>alert("XSS")</script>', ]); $response = $this->actingAs($user)->get('/profile'); $content = $response->content(); // Script tags should be escaped $this->assertStringNotContainsString('<script>', $content); $this->assertStringContainsString('&lt;script&gt;', $content); } public function test_comment_submission_escapes_html() { $post = Post::factory()->create(); $maliciousComment = '<img src=x onerror="alert(1)">'; $response = $this->post("/posts/{$post->id}/comments", [ 'body' => $maliciousComment, ]); // Retrieve the comment $comment = Comment::latest()->first(); // HTML should be escaped $this->assertStringNotContainsString('<img', $comment->body); $this->assertStringNotContainsString('onerror', $comment->body); } public function test_json_responses_prevent_xss() { $response = $this->getJson('/api/user?name=<script>alert(1)</script>'); $response->assertOk(); $response->assertHeader('Content-Type', 'application/json'); // JSON should escape special characters $content = $response->content(); $this->assertStringNotContainsString('<script>', $content); } }

Testing for CSRF Protection

Cross-Site Request Forgery (CSRF) attacks trick users into performing unwanted actions:

<?php namespace Tests\Security; use Tests\TestCase; class CsrfProtectionTest extends TestCase { public function test_post_requests_require_csrf_token() { $user = User::factory()->create(); // Attempt POST without CSRF token $response = $this->actingAs($user)->post('/profile/update', [ 'name' => 'New Name', ]); // Should be rejected (419 status) $response->assertStatus(419); } public function test_delete_requests_require_csrf_token() { $user = User::factory()->create(); $post = Post::factory()->create(['user_id' => $user->id]); // Attempt DELETE without CSRF token $response = $this->actingAs($user) ->delete("/posts/{$post->id}"); $response->assertStatus(419); $this->assertDatabaseHas('posts', ['id' => $post->id]); } public function test_api_routes_exempt_from_csrf() { // API routes should use token authentication instead $response = $this->postJson('/api/data', [ 'key' => 'value', ]); // Should not return CSRF error $this->assertNotEquals(419, $response->status()); } }

Testing Authentication and Authorization

Verify that authentication and authorization controls work correctly:

<?php namespace Tests\Security; use Tests\TestCase; class AuthorizationTest extends TestCase { public function test_guest_cannot_access_protected_routes() { $protectedRoutes = [ ['/dashboard', 'get'], ['/profile', 'get'], ['/posts/create', 'get'], ['/admin', 'get'], ]; foreach ($protectedRoutes as [$route, $method]) { $response = $this->$method($route); $response->assertRedirect('/login'); } } public function test_users_cannot_access_other_users_data() { $user1 = User::factory()->create(); $user2 = User::factory()->create(); $privatePost = Post::factory()->create([ 'user_id' => $user2->id, 'is_private' => true, ]); // User1 should NOT access User2's private post $response = $this->actingAs($user1) ->get("/posts/{$privatePost->id}"); $response->assertForbidden(); } public function test_regular_user_cannot_access_admin_panel() { $user = User::factory()->create(['role' => 'user']); $response = $this->actingAs($user)->get('/admin'); $response->assertForbidden(); } public function test_password_reset_token_cannot_be_reused() { $user = User::factory()->create(); // Request password reset $token = Password::createToken($user); // Use token once $this->post('/reset-password', [ 'token' => $token, 'email' => $user->email, 'password' => 'newpassword123', 'password_confirmation' => 'newpassword123', ]); // Try to reuse same token $response = $this->post('/reset-password', [ 'token' => $token, 'email' => $user->email, 'password' => 'anotherpassword', 'password_confirmation' => 'anotherpassword', ]); $response->assertSessionHasErrors(); } }

Dependency Vulnerability Scanning

Regularly scan dependencies for known vulnerabilities using automated tools:

# Composer security audit (PHP) composer audit # Check for outdated packages composer outdated # NPM audit (JavaScript) npm audit # Fix vulnerabilities automatically npm audit fix # Yarn audit (JavaScript) yarn audit # Snyk CLI (Multi-language) npm install -g snyk snyk auth snyk test snyk monitor # GitHub Dependabot # Enable in repository settings → Security → Dependabot alerts

Testing for Sensitive Data Exposure

Ensure sensitive data is not leaked in responses or logs:

<?php namespace Tests\Security; use Tests\TestCase; class SensitiveDataExposureTest extends TestCase { public function test_passwords_not_included_in_api_responses() { $user = User::factory()->create(); $response = $this->actingAs($user)->getJson('/api/user'); $response->assertOk(); $response->assertJsonMissing(['password' => $user->password]); // Ensure password field is not present at all $data = $response->json(); $this->assertArrayNotHasKey('password', $data); } public function test_credit_card_numbers_are_masked() { $order = Order::factory()->create([ 'card_last_four' => '1234', ]); $response = $this->actingAs($order->user) ->getJson("/api/orders/{$order->id}"); $data = $response->json(); // Full card number should not be present $this->assertArrayNotHasKey('card_number', $data); // Only last 4 digits should be shown $this->assertEquals('****1234', $data['card_display']); } public function test_error_messages_do_not_expose_internals() { // Trigger a database error $response = $this->getJson('/api/invalid-endpoint'); $content = $response->content(); // Should not expose database structure $this->assertStringNotContainsString('SQL', $content); $this->assertStringNotContainsString('table', strtolower($content)); $this->assertStringNotContainsString('/var/www/', $content); // Should show generic error message $this->assertStringContainsString('Not Found', $content); } public function test_api_tokens_are_not_logged() { $user = User::factory()->create(); $token = $user->createToken('test')->plainTextToken; Log::spy(); $this->withHeader('Authorization', "Bearer {$token}") ->getJson('/api/user'); // Ensure token is not logged Log::shouldNotHaveReceived('info') ->with(Mockery::on(function ($message) use ($token) { return strpos($message, $token) !== false; })); } }
Best Practice: Never store sensitive data like passwords, credit cards, or API keys in plain text. Always use encryption, hashing (bcrypt, Argon2), and tokenization where appropriate.

Testing Rate Limiting

Rate limiting prevents brute force attacks and API abuse:

<?php namespace Tests\Security; use Tests\TestCase; class RateLimitingTest extends TestCase { public function test_login_rate_limiting_prevents_brute_force() { $user = User::factory()->create(); // Attempt multiple failed logins for ($i = 0; $i < 10; $i++) { $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'wrong-password', ]); } // Next attempt should be rate limited $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'wrong-password', ]); $response->assertStatus(429); // Too Many Requests $response->assertSee('Too many login attempts'); } public function test_api_rate_limiting() { $user = User::factory()->create(); // Make 60 requests (assuming 60/minute limit) for ($i = 0; $i < 60; $i++) { $response = $this->actingAs($user)->getJson('/api/data'); $response->assertOk(); } // 61st request should be rate limited $response = $this->actingAs($user)->getJson('/api/data'); $response->assertStatus(429); // Should include retry-after header $this->assertNotNull($response->headers->get('Retry-After')); } }

Testing for Security Misconfigurations

Verify that your application is properly configured:

<?php namespace Tests\Security; use Tests\TestCase; class SecurityConfigurationTest extends TestCase { public function test_debug_mode_disabled_in_production() { if (app()->environment('production')) { $this->assertFalse(config('app.debug'), 'Debug mode should be disabled in production' ); } } public function test_https_enforced_in_production() { if (app()->environment('production')) { $response = $this->get('/'); // Check if HTTPS is enforced $this->assertTrue( $response->isRedirect() || request()->secure(), 'HTTPS should be enforced in production' ); } } public function test_security_headers_present() { $response = $this->get('/'); // Check for security headers $response->assertHeader('X-Frame-Options', 'SAMEORIGIN'); $response->assertHeader('X-Content-Type-Options', 'nosniff'); $response->assertHeader('X-XSS-Protection', '1; mode=block'); // Content Security Policy $this->assertNotNull($response->headers->get('Content-Security-Policy')); } public function test_sensitive_files_not_accessible() { $sensitiveFiles = [ '/.env', '/phpinfo.php', '/.git/config', '/composer.json', '/package.json', ]; foreach ($sensitiveFiles as $file) { $response = $this->get($file); $response->assertNotFound(); } } public function test_default_credentials_changed() { // Ensure no default admin accounts exist $this->assertDatabaseMissing('users', [ 'email' => 'admin@admin.com', ]); $this->assertDatabaseMissing('users', [ 'username' => 'admin', 'password' => bcrypt('admin'), ]); } }

Penetration Testing Basics

Manual penetration testing helps discover vulnerabilities automated tools might miss:

# Common penetration testing techniques: # 1. Directory Traversal https://example.com/download?file=../../etc/passwd https://example.com/uploads/../../../config/database.php # 2. Parameter Tampering https://example.com/user/profile?id=123 # Try: ?id=124 (access other user's data) # 3. Mass Assignment POST /api/users/123 { "name": "John", "is_admin": true // Attempt privilege escalation } # 4. Insecure Direct Object References (IDOR) GET /api/orders/5001 // User's own order GET /api/orders/5002 // Try accessing other user's order # 5. Command Injection https://example.com/ping?host=google.com;cat /etc/passwd https://example.com/backup?file=data.zip && rm -rf / # 6. LDAP Injection username: admin)(&(password=*)) password: anything # 7. XML External Entity (XXE) <?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <data>&xxe;</data>
Warning: Only perform penetration testing on applications you own or have explicit written permission to test. Unauthorized testing is illegal and unethical.

Automated Security Scanning Tools

Use automated tools to continuously scan for vulnerabilities:

# OWASP ZAP (Zed Attack Proxy) # Open-source web app security scanner docker run -t owasp/zap2docker-stable zap-baseline.py -t https://example.com # Nikto - Web server scanner nikto -h https://example.com # Nmap - Network scanner nmap -sV -sC example.com # SQLMap - SQL injection testing sqlmap -u "https://example.com/search?q=test" --batch --banner # Burp Suite Community Edition # Download from: https://portswigger.net/burp/communitydownload # Wapiti - Web vulnerability scanner wapiti -u https://example.com # Arachni - Web application security scanner arachni https://example.com --checks=* # SonarQube - Code quality and security # Integrates with CI/CD pipelines docker run -d --name sonarqube -p 9000:9000 sonarqube:latest
Exercise 1: Create a comprehensive security test suite that checks for: (1) SQL injection in all form inputs, (2) XSS in user-generated content, (3) CSRF protection on state-changing operations, (4) proper authorization on API endpoints, and (5) sensitive data exposure in logs.
Exercise 2: Set up automated dependency scanning in your CI/CD pipeline using Snyk or GitHub Dependabot. Configure it to fail builds if high-severity vulnerabilities are detected.
Exercise 3: Perform a security audit of your application using OWASP ZAP. Document all findings with severity levels (critical, high, medium, low) and create a remediation plan with priorities.

Summary

In this lesson, we've covered essential security testing practices:

  • Understanding OWASP Top 10 vulnerabilities and how to test for them
  • Testing for SQL injection, XSS, and CSRF vulnerabilities
  • Verifying authentication and authorization controls
  • Scanning dependencies for known vulnerabilities
  • Testing for sensitive data exposure in responses and logs
  • Implementing and testing rate limiting to prevent abuse
  • Verifying security configurations and headers
  • Using automated security scanning tools
  • Basic penetration testing techniques

Security is not a one-time effort but an ongoing process. Integrate security testing into your development workflow, stay informed about emerging threats, and regularly update your dependencies and security practices.