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.
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
.envfile in the repository exposes your full application configuration. - Critical external scan findings. Debug mode enabled in production, exposed
.envfile 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:
-
Add
composer auditto your existing CI workflow. This takes five minutes and catches the most impactful issues (known vulnerabilities in your dependencies). -
Add secret detection with Gitleaks. This prevents the most embarrassing class of security incident: committed credentials.
-
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.
Related Articles
Securing Laravel Horizon in Production: A Complete Guide
Laravel Horizon exposes your entire queue system, including job payloads, failed jobs with user data, and worker status. Here is how to lock it down properly in production.
SecurityLaravel Debug Mode in Production: What Attackers See
18% of Laravel apps run debug mode in production. Attackers use exposed stack traces, environment variables, and database credentials to compromise your app.
SecurityOWASP Top 10 in Laravel: Real Vulnerabilities, Real Code Fixes (2026)
SQL injection through raw queries. XSS from unescaped Blade output. CSRF bypasses on API routes. Every OWASP Top 10 category mapped to Laravel-specific vulnerabilities with code you can copy to fix them.
Compare StackShield
Security Checklists
Laravel Production Deployment Security Checklist
A comprehensive security checklist for deploying Laravel applications to production. Covers environment config, server hardening, access control, and monitoring.
20 itemsLaravel API Security Checklist
Secure your Laravel API endpoints against common vulnerabilities. Covers authentication, input validation, rate limiting, and response security.
Stay Updated on Laravel Security
Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.