Security Scanning a PHP + React App from Scratch

From zero to automated scanning in one session

Case Study — April 2026 — ← Back to Research — Companion to: Free Security Scanning Guide

This is a real walkthrough of applying the Free Security Scanning Guide to a production codebase. The app is a PHP web application with a React frontend, running on AWS (EC2, RDS, S3, ALB). We started with no Docker installed and no prior security scanning.

1. Starting Point

2. Installing Docker

Docker Desktop was not present. Installed via Homebrew:

brew install --cask docker

This installed Docker Desktop 4.67.0 for Mac (arm64). After install, launched the daemon:

open -a Docker

First launch took ~45 seconds to initialise. Verified with docker info.

Note: Docker Desktop must be running (the whale icon in the menu bar) before any docker run commands will work. On first install, it asks you to accept the EULA.

3. Running the Scans

Gotcha: Source Code Location

The guide assumes your code is in the repo root. This app's code lives under app/ and its subdirectories:

app/
  config/
  src/
  the_app/
    public_html/        <- PHP app root
      class/            <- PHP classes, vendor libs
      admin/            <- admin UI
      client/           <- client-facing pages
      react-app/        <- React frontend

Mounting the repo root would scan deploy scripts, infra templates, and docs — not useful. We mounted app/ directly.

Gotcha: Scanner CLI Syntax Change

The guide's original command failed with v6.18.0 of the Semgrep image. The scanner now expects --target-dir and --artifact-dir flags:

# This failed:
docker run --rm --pull always -v "$(pwd):/code" \
  registry.gitlab.com/security-products/semgrep \
  /analyzer run /code

# This worked:
docker run --rm --pull always -v "$(pwd)/app:/code" \
  registry.gitlab.com/security-products/semgrep \
  /analyzer run --target-dir /code --artifact-dir /code

Gotcha: Gemnasium Depth Limit

The dependency scanner only looks 2 directories deep by default. It warned that the_app/public_html subdirectories were skipped, meaning lockfiles in the React app directory were missed. Fix with DS_MAX_DEPTH:

docker run --rm --pull always \
  -e DS_MAX_DEPTH=5 \
  -v "$(pwd)/app:/code" \
  registry.gitlab.com/security-products/gemnasium \
  /analyzer run --target-dir /code --artifact-dir /code

Gotcha: Minified JS Timeouts

The SAST scanner timed out on large minified JavaScript files (jQuery, Highcharts, TinyMCE). These are third-party vendor files — timeouts are expected and harmless. Use SAST_EXCLUDED_PATHS to skip them in future scans.

4. Results Summary

SAST (Source Code Vulnerabilities)

SeverityCount
Critical12
High16
Medium121
Total149

Dependency Scanning (Known CVEs)

SeverityCount
Critical3
High10
Medium5
Total18

167 total findings. Sounds alarming. But most are noise.

5. Triaging the Findings

Critical SAST: 12 findings, only 2 real

10 in PHPMailer library — the scanner flagged escapeshellcmd() usage in vendored PHPMailer. These are false positives in context; PHPMailer handles this internally.

2 in file export — a real command injection vulnerability. User-derived values were passed directly to a PHP shell function without sanitisation.

High SAST: 16 findings, all vendor

All 16 were dynamic code evaluation in third-party JavaScript libraries (jQuery, Underscore, TinyMCE). These use dynamic evaluation as part of their normal operation. Low real-world risk.

Medium SAST: 121 findings

Finding TypeCountAssessment
Non-literal regex52Vendor JS. Low risk
Weak hash (MD5/SHA1)29PHP checksums, not security. Review needed
Incorrect regex28Vendor JS. Low risk
Cross-site Scripting9React innerHTML — admin content, acceptable
Dangerous function2Needs context review
Info exposure1Likely phpinfo()

XSS: All Acceptable

All 9 XSS findings were React rendering admin-authored HTML from the database (custom messages and footers). The content is controlled by administrators, not end users. Acceptable risk.

Dependency Findings: Build-time Only

All Critical and High dependency findings were in Node.js build-time packages (webpack, minimist, braces). They don't ship to production — they only run during the build process.

6. Fixing the Vulnerabilities

Command Injection Fix

The file export endpoint was passing user-derived values straight into a PHP shell function. Fixed with escapeshellarg() to wrap each value in safe single quotes:

$safeTempSvg = escapeshellarg("temp/$tempName.svg");
$safeOutfile = escapeshellarg($outfile);
$command = "java -jar " . escapeshellarg(CONVERTER_PATH)
  . " $typeString -d $safeOutfile $width $safeTempInput";

$typeString and $width were already safe — $typeString is set from a hardcoded whitelist, and $width is cast to (int).

Dependency Fix

Ran npm audit fix in the React app directory:

Housekeeping

Added scan artifacts to .gitignore so reports never get committed:

# Security scan artifacts
gl-*.json
*.sarif
*sbom*.json
.semgrepignore

7. Automating Future Scans

Set up a Claude Code PostToolUse hook that triggers after every git commit. The hook:

  1. Runs the Semgrep SAST scanner (with vendor exclusions)
  2. Parses the JSON report
  3. Feeds Critical/High findings back to Claude Code as context
  4. Claude Code automatically triages and suggests fixes

Every commit now gets automatic security scanning. No manual intervention needed.

8. Lessons Learned

IssueResolution
Docker not installedbrew install --cask docker + open -a Docker
Guide's CLI syntax outdatedUse --target-dir and --artifact-dir flags
Scanner mounted wrong directoryMount app/ not repo root
Gemnasium missed deep lockfilesSet DS_MAX_DEPTH=5
Timeouts on minified vendor JSExpected; use SAST_EXCLUDED_PATHS to skip
149 findings look alarmingOnly 2 real Criticals in custom code; rest are vendor
React innerHTML XSS flagsAdmin-authored DB content, not user input
npm audit fix only partialRemaining vulns need webpack 4 to 5 (separate task)
Scan artifacts in working treeAdded to .gitignore

9. The Bottom Line

167 findings. 2 real vulnerabilities. 1 hour from zero to automated scanning.

The scanners are free, the Docker images are public, and Claude Code turns a wall of JSON into a prioritised, actionable fix list. The hardest part was getting the Docker command syntax right — and this case study saves you that trouble.


Read the companion guide for the step-by-step instructions without the narrative.