Laravel Security Scans in GitHub Actions: A CI/CD Pipeline Guide
Automate Laravel security checks in your GitHub Actions pipeline. Set up composer audit, static analysis, StackShield post-deploy scans, and block merges when vulnerabilities are found.
Most Laravel teams have a test pipeline. They run PHPUnit on every PR, check code style, maybe run PHPStan. But security checks? Those are still mostly manual, run occasionally, or skipped entirely.
This guide walks through building a layered security pipeline in GitHub Actions that catches vulnerabilities at every stage: dependency scanning on PR, static analysis before merge, secret detection on push, and an external scan after deploy.
Why CI/CD Security Matters
The problem with one-off security reviews is that they measure a snapshot in time. Your application changes constantly. Every PR can introduce a new dependency with a known CVE.
CI/CD security catches issues at the moment they are introduced:
- A PR that adds a vulnerable package fails the build before merge
- A commit with an accidentally staged
.envgets blocked immediately - A deploy to staging triggers an external scan, blocking promotion to production if issues are found
Layer 1: Dependency Scanning with composer audit
composer audit checks your composer.lock against the PHP Security Advisories Database.
name: Security
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
dependency-audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
- run: composer install --prefer-dist --no-progress
- name: Audit dependencies
run: composer audit
If any package has a known advisory, the job fails and the PR cannot be merged.
Ignoring Known Advisories
- name: Audit dependencies
run: |
# CVE-2024-12345: Affects XML parser only, which we do not use.
composer audit --ignore CVE-2024-12345
Layer 2: Static Analysis
PHPStan catches type errors and security patterns before code runs:
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
- run: composer install --prefer-dist --no-progress
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse --error-format=github
The --error-format=github flag outputs errors as inline annotations on the PR diff.
Layer 3: Secret Detection
TruffleHog scans git history for accidentally committed credentials:
secret-detection:
name: Secret Detection
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --only-verified
The --only-verified flag reduces false positives by only reporting secrets that TruffleHog can confirm are active.
Layer 4: Post-Deploy External Scanning
After deploying to staging, trigger a StackShield scan via the API:
post-deploy-scan:
name: StackShield Scan
runs-on: ubuntu-latest
needs: [deploy-staging]
if: github.ref == 'refs/heads/main'
steps:
- name: Trigger scan
id: trigger
run: |
RESPONSE=$(curl -s -X POST https://stackshield.app/api/v1/scans \
-H "Authorization: Bearer ${{ secrets.STACKSHIELD_API_KEY }}" \
-H "Content-Type: application/json" \
-d "{\"url\": \"${{ secrets.STAGING_URL }}\"}")
SCAN_ID=$(echo $RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
echo "scan_id=$SCAN_ID" >> $GITHUB_OUTPUT
- name: Wait for results
run: |
for i in $(seq 1 20); do
sleep 10
RESULT=$(curl -s \
-H "Authorization: Bearer ${{ secrets.STACKSHIELD_API_KEY }}" \
"https://stackshield.app/api/v1/scans/${{ steps.trigger.outputs.scan_id }}")
STATUS=$(echo $RESULT | python3 -c "import json,sys; print(json.load(sys.stdin)['status'])")
if [ "$STATUS" = "completed" ]; then
CRITICAL=$(echo $RESULT | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(len([f for f in d.get('findings',[]) if f.get('severity')=='critical']))
")
[ "$CRITICAL" -gt "0" ] && { echo "::error::$CRITICAL critical finding(s)"; exit 1; }
echo "Scan passed."; exit 0
fi
done
echo "::warning::Scan timed out"
When Each Layer Runs
| Layer | Trigger | Blocks |
|---|---|---|
| Dependency audit | Every PR and push to main | PR merge |
| Static analysis | Every PR and push to main | PR merge |
| Secret detection | Every push | Push (on PR) |
| StackShield scan | After deploy to staging | Promotion to production |
Branch Protection Rules
- Go to repository Settings, then Branches
- Edit your branch protection rule for
main - Check "Require status checks to pass before merging"
- Add job names: Dependency Audit, Static Analysis, Secret Detection
- Check "Do not allow bypassing the above settings"
No PR can be merged unless all security jobs pass.
Start With One Layer
If this feels like a lot, start with composer audit. It takes five minutes to set up and catches real vulnerabilities in existing dependencies immediately. Add the other layers over time.
Run a free StackShield scan to see what your application looks like from the outside before you wire it into your pipeline.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.
Frequently Asked Questions
How do I run composer audit in GitHub Actions?
Add a job to your workflow that runs composer install followed by composer audit. If any advisories are found, composer audit exits with a non-zero code which automatically fails the GitHub Actions job.
Can I block a PR if a security vulnerability is found?
Yes. Set up a required status check in your repository branch protection rules. Go to Settings, then Branches, then edit your protection rule. Under "Require status checks to pass before merging", add your security workflow job name.
How do I trigger a StackShield scan after deployment?
Use the StackShield API from a GitHub Actions step that runs after your deploy step. Send a POST request with your API key and the target URL, then poll the results endpoint until the scan completes. If critical findings are returned, exit with a non-zero code to fail the pipeline.
Should security scans run on every PR or only on merge?
Run dependency audits and static analysis on every PR so developers get feedback before code is merged. Reserve post-deploy external scans for after deployment to staging, since those target a running application.
How do I ignore known vulnerabilities in CI?
For composer audit, pass specific CVE IDs with --ignore CVE-XXXX-XXXXX to suppress known issues you have accepted. Document every ignored advisory in a comment or linked issue so future team members understand why it was excluded.
Related Articles
Laravel Session Security: Cookies, Hijacking & config/session.php
A deep dive into Laravel session security. Learn how cookie flags, session drivers, and config/session.php settings protect against hijacking, fixation, and sidejacking attacks.
SecurityAutomated 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.
SecurityLaravel Content Security Policy: Configure CSP Without Breaking Your App
Only 22% of Laravel apps have a Content Security Policy. Learn how to implement CSP with spatie/laravel-csp, handle Livewire and Vite nonces, and avoid the mistakes that break production.
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.