Security 14 min read

Automated Security Testing in Laravel CI/CD Pipelines

How to add security gates to your Laravel CI/CD pipeline with GitHub Actions. Covers dependency scanning, static analysis, secret detection, and automated security monitoring.

Matt King
Matt King
May 7, 2026
Last updated: May 7, 2026
Automated Security Testing in Laravel CI/CD Pipelines

Your Laravel application passes all its tests, the code review is approved, and the deployment goes out. Thirty minutes later, someone discovers the new environment variable for a third-party API key was committed to the repository, or a composer update pulled in a package with a known RCE vulnerability.

Security testing in CI/CD pipelines prevents these scenarios by catching issues automatically before they reach production. This guide shows you how to build a layered security pipeline for Laravel applications using GitHub Actions, with practical YAML configurations you can copy and adapt.

What this guide covers:

  • Dependency vulnerability scanning with composer audit
  • Static analysis security rules with PHPStan
  • Secret detection to prevent credential leaks
  • External security scanning as a deployment gate
  • Docker image scanning
  • How to prioritize blocking vs. advisory checks

The Security Pipeline Architecture

A well-structured security pipeline has multiple layers, each catching a different class of issue:

Code Push
  |
  v
[Layer 1: Secret Detection] -- Block if secrets found
  |
  v
[Layer 2: Dependency Audit]  -- Block on critical/high CVEs
  |
  v
[Layer 3: Static Analysis]   -- Block on security-critical rules
  |
  v
[Layer 4: Application Tests] -- Block on failure
  |
  v
[Layer 5: Build & Deploy]
  |
  v
[Layer 6: External Scan]     -- Block on critical findings

Layers 1 through 4 run before deployment. Layer 6 runs after deployment to staging (or production, depending on your workflow) and verifies the running application is properly configured.


Layer 1: Secret Detection

The most important CI/CD security check is preventing secrets from being committed to your repository. Once a secret is in your git history, rotating it is the only safe option.

GitHub's built-in secret scanning

If you are using GitHub, enable secret scanning in your repository settings. It detects known secret patterns (AWS keys, Stripe keys, database connection strings) and alerts you automatically.

Gitleaks for comprehensive detection

For more thorough detection, add Gitleaks to your pipeline:

name: Security Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  secret-detection:
    name: Secret Detection
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for scanning

      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Custom rules for Laravel

Create a .gitleaks.toml configuration that catches Laravel-specific secrets:

title = "Laravel Gitleaks Config"

[[rules]]
id = "laravel-app-key"
description = "Laravel APP_KEY"
regex = '''APP_KEY=base64:[A-Za-z0-9+/=]{44}'''
tags = ["laravel", "key"]

[[rules]]
id = "laravel-db-password"
description = "Laravel database password in code"
regex = '''DB_PASSWORD=.{3,}'''
path = '''(?i)(?!\.env\.example)'''
tags = ["laravel", "database"]

[[rules]]
id = "env-file-committed"
description = ".env file committed to repo"
path = '''\.env$'''
tags = ["laravel", "env"]

[allowlist]
paths = [
    '''\.env\.example$''',
    '''\.env\.testing$''',
]

This is a hard gate. If any secret is detected, the pipeline should fail immediately.


Layer 2: Dependency Vulnerability Scanning

Composer audit

Since Composer 2.4, composer audit checks your installed packages against the PHP Security Advisories Database. This is the simplest and most effective dependency check for Laravel:

  dependency-audit:
    name: Dependency Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run Composer Audit
        run: composer audit --format=json | tee audit-results.json

      - name: Check for critical vulnerabilities
        run: |
          CRITICAL=$(cat audit-results.json | jq '[.advisories[][] | select(.severity == "critical" or .severity == "high")] | length')
          if [ "$CRITICAL" -gt 0 ]; then
            echo "::error::Found $CRITICAL critical/high severity vulnerabilities"
            cat audit-results.json | jq '.advisories[][] | select(.severity == "critical" or .severity == "high") | {package: .packageName, title: .title, severity: .severity, cve: .cve}'
            exit 1
          fi
          echo "No critical or high vulnerabilities found"

This configuration blocks on critical and high severity vulnerabilities but allows medium and low to pass. For a deeper look at managing Composer vulnerabilities, see our Composer vulnerability management guide.

OWASP Dependency-Check

For more comprehensive scanning that covers the full NVD database (not just PHP-specific advisories), add OWASP Dependency-Check:

      - name: OWASP Dependency-Check
        uses: dependency-check/Dependency-Check_Action@main
        with:
          project: 'laravel-app'
          path: '.'
          format: 'JSON'
          args: >-
            --enableExperimental
            --suppression dependency-check-suppression.xml

      - name: Upload Dependency-Check results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: dependency-check-report
          path: reports/

Layer 3: Static Analysis with Security Rules

PHPStan and Psalm are not just code quality tools. With the right rulesets, they catch security vulnerabilities at the code level before your application runs.

PHPStan security configuration

Install PHPStan with security-focused extensions:

composer require --dev phpstan/phpstan phpstan/phpstan-strict-rules

Create a phpstan.neon with security-relevant rules:

includes:
    - vendor/phpstan/phpstan-strict-rules/rules.neon

parameters:
    level: 6
    paths:
        - app
        - config
        - routes

    # Security-relevant checks
    checkMissingIterableValueType: true
    checkGenericClassInNonGenericObjectType: false
    reportUnmatchedIgnoredErrors: true

Add it to your pipeline:

  static-analysis:
    name: Static Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run PHPStan
        run: vendor/bin/phpstan analyse --error-format=github --no-progress

Custom security rules

Create custom PHPStan rules to catch Laravel-specific security issues. For example, detecting raw database queries:

<?php
// phpstan-rules/NoRawDatabaseQueries.php

namespace App\PHPStan\Rules;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;

/**
 * @implements Rule<MethodCall>
 */
class NoRawDatabaseQueries implements Rule
{
    public function getNodeType(): string
    {
        return MethodCall::class;
    }

    public function processNode(Node $node, Scope $scope): array
    {
        if (!$node->name instanceof Node\Identifier) {
            return [];
        }

        $dangerousMethods = ['selectRaw', 'whereRaw', 'havingRaw', 'orderByRaw', 'groupByRaw'];

        if (in_array($node->name->name, $dangerousMethods, true)) {
            return [
                sprintf(
                    'Usage of %s() detected. Ensure user input is parameterized to prevent SQL injection.',
                    $node->name->name
                ),
            ];
        }

        return [];
    }
}

This does not block the pipeline, but it flags every raw query for review to confirm user input is properly parameterized.


Layer 4: Preventing .env File Commits

Beyond secret scanning, add a specific check that prevents .env files from being committed:

  env-check:
    name: Environment File Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check for .env files
        run: |
          ENV_FILES=$(find . -name ".env" -not -name ".env.example" -not -name ".env.testing" -not -path "./.git/*" | head -20)
          if [ -n "$ENV_FILES" ]; then
            echo "::error::Found .env files that should not be committed:"
            echo "$ENV_FILES"
            exit 1
          fi
          echo "No .env files found in repository"

Also ensure your .gitignore is correct:

      - name: Verify .gitignore includes .env
        run: |
          if ! grep -q "^\.env$" .gitignore; then
            echo "::error::.gitignore does not include .env"
            exit 1
          fi

Layer 5: Docker Image Scanning

If you deploy with Docker, scan your images for OS-level vulnerabilities before pushing them to your registry:

  docker-scan:
    name: Docker Image Scan
    runs-on: ubuntu-latest
    needs: [secret-detection, dependency-audit, static-analysis]
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'table'
          exit-code: '1'
          severity: 'CRITICAL,HIGH'
          ignore-unfixed: true

Trivy scans the OS packages, language-specific dependencies, and configuration files inside your Docker image. Setting exit-code: '1' makes it a hard gate for critical and high severity issues.


Layer 6: External Security Scanning Post-Deploy

After deploying to staging (or production), run an external security scan to verify the running application is properly configured. This catches issues that code analysis cannot: exposed debug mode, missing security headers, accessible development tools, and misconfigured web servers.

StackShield CI/CD integration

StackShield can run as a deployment gate that scans your live application:

  external-scan:
    name: External Security Scan
    runs-on: ubuntu-latest
    needs: [deploy-staging]
    steps:
      - name: Run StackShield scan
        env:
          STACKSHIELD_API_KEY: ${{ secrets.STACKSHIELD_API_KEY }}
        run: |
          RESULT=$(curl -s -X POST https://api.stackshield.io/v1/scan \
            -H "Authorization: Bearer $STACKSHIELD_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{"url": "https://staging.yourapp.com"}')

          CRITICAL=$(echo $RESULT | jq '[.findings[] | select(.severity == "critical")] | length')

          if [ "$CRITICAL" -gt 0 ]; then
            echo "::error::StackShield found $CRITICAL critical security issues"
            echo $RESULT | jq '.findings[] | select(.severity == "critical")'
            exit 1
          fi

          echo "External scan passed"
          echo $RESULT | jq '.summary'

For full setup instructions, see the StackShield CI/CD documentation and the GitHub Actions integration guide.


The Complete Workflow File

Here is the full GitHub Actions workflow combining all layers:

name: Security Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

permissions:
  contents: read
  security-events: write

jobs:
  secret-detection:
    name: Secret Detection
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Run Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  dependency-audit:
    name: Dependency Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Composer Audit
        run: |
          composer audit --format=json | tee audit-results.json
          CRITICAL=$(cat audit-results.json | jq '[.advisories[][] | select(.severity == "critical" or .severity == "high")] | length // 0')
          if [ "$CRITICAL" -gt 0 ]; then
            echo "::error::Found $CRITICAL critical/high vulnerabilities"
            exit 1
          fi

  static-analysis:
    name: Static Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Run PHPStan
        run: vendor/bin/phpstan analyse --error-format=github --no-progress

  env-check:
    name: Environment File Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check for .env files
        run: |
          ENV_FILES=$(find . -name ".env" -not -name ".env.example" -not -name ".env.testing" -not -path "./.git/*" | head -20)
          if [ -n "$ENV_FILES" ]; then
            echo "::error::Found .env files committed to repository"
            exit 1
          fi

  deploy-staging:
    name: Deploy to Staging
    needs: [secret-detection, dependency-audit, static-analysis, env-check]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: echo "Deploy to staging here"

  external-scan:
    name: External Security Scan
    needs: [deploy-staging]
    runs-on: ubuntu-latest
    steps:
      - name: Run StackShield scan
        env:
          STACKSHIELD_API_KEY: ${{ secrets.STACKSHIELD_API_KEY }}
        run: |
          RESULT=$(curl -s -X POST https://api.stackshield.io/v1/scan \
            -H "Authorization: Bearer $STACKSHIELD_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{"url": "https://staging.yourapp.com"}')
          CRITICAL=$(echo $RESULT | jq '[.findings[] | select(.severity == "critical")] | length')
          if [ "$CRITICAL" -gt 0 ]; then
            echo "::error::Critical security issues found"
            exit 1
          fi

Prioritizing Blocking vs. Advisory Checks

Not every security check should block your deployment. Here is a practical framework:

Hard gates (block the pipeline)

These should always prevent deployment:

  • Leaked secrets or credentials. There is no acceptable risk here. A committed secret must be rotated immediately.
  • Critical/high dependency vulnerabilities. Known RCE or authentication bypass CVEs in your dependencies are exploitable today.
  • Committed .env files. A .env file in the repository exposes your full application configuration.
  • Critical external scan findings. Debug mode enabled in production, exposed .env file via HTTP, ungated Telescope or Horizon.

Advisory checks (warn but do not block)

These should generate notifications but allow deployment to proceed:

  • Medium/low dependency vulnerabilities. Important to track and fix, but unlikely to be exploited immediately.
  • Static analysis warnings. Code quality issues and potential problems that need review but are not actively exploitable.
  • Informational scan findings. Missing optional security headers, non-critical configuration suggestions.

Implementation

Use GitHub Actions' continue-on-error for advisory checks:

  advisory-checks:
    name: Advisory Security Checks
    runs-on: ubuntu-latest
    continue-on-error: true  # Warnings only, does not block
    steps:
      - uses: actions/checkout@v4
      - name: Check medium/low vulnerabilities
        run: |
          composer audit --format=json | tee audit-results.json
          MEDIUM=$(cat audit-results.json | jq '[.advisories[][] | select(.severity == "medium" or .severity == "low")] | length // 0')
          if [ "$MEDIUM" -gt 0 ]; then
            echo "::warning::Found $MEDIUM medium/low vulnerabilities to review"
          fi

Getting Started

If you have no security checks in your pipeline today, do not try to add everything at once. Start with these three steps in order:

  1. Add composer audit to your existing CI workflow. This takes five minutes and catches the most impactful issues (known vulnerabilities in your dependencies).

  2. Add secret detection with Gitleaks. This prevents the most embarrassing class of security incident: committed credentials.

  3. Add a post-deploy StackShield scan on your staging environment. This catches configuration issues that only manifest in a running application.

Once these three are running reliably, layer in static analysis, Docker scanning, and more granular severity filtering. The goal is a pipeline that catches critical security issues without generating so much noise that your team starts ignoring the alerts.

For detailed integration setup, see the StackShield CI/CD documentation and the GitHub Actions integration guide.

Frequently Asked Questions

Which security checks should block a deployment?

Checks that should block deployments (hard gates) include: critical and high severity dependency vulnerabilities, leaked secrets or credentials in code, and critical findings from external security scans. Advisory checks that should warn but not block include: medium and low dependency vulnerabilities, code style issues, and informational static analysis findings. Start strict and relax as your team adjusts.

How do I run composer audit in GitHub Actions?

Add a step to your workflow that runs "composer audit --format=json". The command exits with a non-zero code if vulnerabilities are found, which automatically fails the GitHub Actions step. You can parse the JSON output to filter by severity level if you only want to block on critical vulnerabilities.

Can StackShield integrate with my CI/CD pipeline?

Yes. StackShield provides a CI/CD integration that runs a security scan against your staging or production URL as part of your deployment pipeline. If critical security issues are detected (exposed .env, debug mode enabled, ungated development tools), the pipeline fails before the deployment completes. See the documentation at stackshield.io/docs/cicd for setup instructions.

What is the difference between SAST and DAST for Laravel?

SAST (Static Application Security Testing) analyzes your source code without running the application. Tools like PHPStan and Psalm catch issues like SQL injection patterns, unsafe deserialization, and type errors. DAST (Dynamic Application Security Testing) tests the running application from the outside. Tools like StackShield and OWASP ZAP check for exposed endpoints, missing headers, and configuration issues. You need both: SAST catches code-level bugs before deployment, DAST catches configuration and runtime issues after deployment.

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.