Combining static and dynamic analysis the right way
Teams that build Magento applications without systematic security testing rely on luck instead of tools that reliably surface vulnerabilities. Static analysis with PHPStan and Psalm catches dangerous data flows directly in the source code, while dynamic scanners like OWASP ZAP run real attacks against a running staging environment. This article shows how both approaches work together, where their limits lie, and when each investment pays off.
Table of Contents
- 1. Static vs. dynamic analysis: the underlying mechanics
- 2. How SAST works: AST, control flow and taint tracking
- 3. How DAST works: crawling, fuzzing and real HTTP attacks
- 4. What SAST catches and what DAST finds: blind spots compared
- 5. False positive rates and tuning
- 6. Integrating SAST into the CI pipeline: PHPStan security rules
- 7. Psalm taint analysis: tracking data flow from source to sink
- 8. DAST against a running staging environment
- 9. SAST and DAST head to head
- 10. Summary
- 11. FAQ
1. Static vs. dynamic analysis: the underlying mechanics
SAST (Static Application Security Testing) and DAST (Dynamic Application Security Testing) solve the same problem, finding vulnerabilities before an attacker does, through fundamentally different mechanics. SAST analyzes source code without executing it: a parser builds an Abstract Syntax Tree, then an analyzer traces control flow and data flow across functions, classes and call chains. The tool sees every line of code, regardless of whether it is ever reached at runtime. DAST instead treats the application as a black box: it sends real HTTP requests against a running instance and evaluates the actual responses, exactly as an external attacker would.
The difference is clearest with complex code: SAST flags a SQL injection as soon as unsanitized user input reaches a query string, regardless of whether that code path is ever invoked in practice. DAST only catches the same flaw if the scanner actually finds the affected endpoint and tests it with a matching payload, but it evaluates the finding in the real runtime context, with an active session and a genuine HTTP response.
2. How SAST works: AST, control flow and taint tracking
Modern SAST tools for PHP such as PHPStan and Psalm operate on several layers at once. First the code is turned into an Abstract Syntax Tree that captures the syntactic structure of every file. On top of that, the analyzer computes a control flow graph to understand which code paths are reachable under which conditions. The actual security check happens through taint analysis: variables originating from an untrusted source such as $_GET or a request object are marked as tainted and tracked through the entire data flow.
If such a tainted variable reaches a dangerous sink such as a SQL query, an echo call, or an unserialize() function without first passing through a recognized sanitizer, the tool reports a finding. This mechanism works purely on the code, requires no running application and no test data, but it does need precise type information and well maintained stub definitions for frameworks like Magento.
<?php
declare(strict_types=1);
namespace Mironsoft\SeoSuite\Model;
use Magento\Framework\App\ResourceConnection;
/**
* Example: SAST-detectable SQL injection via string concatenation.
* A static analyzer flags this because tainted request data flows
* directly into a raw SQL query without going through a sanitizer.
*/
final class ProductLookup
{
public function __construct(
private readonly ResourceConnection $resourceConnection
) {
}
/**
* VULNERABLE: user-controlled $sku is concatenated into raw SQL.
* Taint source: HTTP request parameter.
* Taint sink: Zend_Db_Adapter::query().
*
* @param string $sku Raw SKU value taken directly from $_GET.
* @return array<int, array<string, mixed>>
*/
public function findBySkuUnsafe(string $sku): array
{
$connection = $this->resourceConnection->getConnection();
// TaintedSql: never build queries via string concatenation
$query = "SELECT * FROM catalog_product_entity WHERE sku = '" . $sku . "'";
return $connection->fetchAll($query);
}
/**
* SAFE: parameter binding removes the taint before it reaches the sink.
*
* @param string $sku SKU value, still untrusted at this point.
* @return array<int, array<string, mixed>>
*/
public function findBySkuSafe(string $sku): array
{
$connection = $this->resourceConnection->getConnection();
$select = $connection->select()
->from('catalog_product_entity')
->where('sku = ?', $sku);
return $connection->fetchAll($select);
}
}
3. How DAST works: crawling, fuzzing and real HTTP attacks
A DAST scanner starts with no knowledge of the source code and has to discover the attack surface itself. In the first step, the tool crawls the application like a browser: it follows links, fills in forms, calls JavaScript-generated routes, and logs every endpoint it finds along with its parameters. This catalog becomes the site tree, the basis for the subsequent active scan, where every parameter is fed with a library of known attack payloads, for example SQL metacharacters, XSS payloads, or path traversal sequences.
Unlike SAST, DAST evaluates the actual behavior of the system: changed response times, different status codes, reflected payloads in the HTML, or SQL error messages in the response body all count as evidence of a real, exploitable vulnerability. This black box perspective is technology agnostic, so it works regardless of whether the application is written in PHP, Java, or Node.js, but it only ever finds what the crawler actually reaches and can test with the credentials it has available.
4. What SAST catches and what DAST finds: blind spots compared
SAST excels at vulnerability classes that follow directly from data flow in the code: SQL injection through string concatenation, cross-site scripting through missing output encoding, unsafe deserialization, and hardcoded credentials. The key advantage is completeness: a code path reached only under a rare error condition is still analyzed, because SAST does not depend on actual traffic. The flip side: runtime configuration, server headers, TLS settings, or the interaction between multiple microservices sit outside the field of view of a pure code scanner.
DAST covers exactly that gap: missing security headers, weak TLS configuration, session fixation, authentication bypasses through manipulated cookies, and misconfigurations that only arise from the interplay of web server, framework, and infrastructure. What DAST systematically misses in turn is code paths the crawler never reaches, such as admin functions without a linked entry point, plus deeper business logic flaws that no generic payload triggers. Both tool classes complement each other rather than replacing one another.
5. False positive rates and tuning
SAST tools tend toward high false positive rates without tuning, because they conservatively flag every potential data flow, even when sanitization happens at a point the analyzer does not recognize. A typical example: a custom escaping helper module that PHPStan or Psalm does not know as a trusted sanitizer. The fix lies in baselines, which freeze the current set of findings so only new violations break the build, combined with explicit annotations such as @psalm-taint-escape that tell the analyzer which functions actually clean the data.
False positives are rarer with DAST scanners, but more expensive to triage, because every finding must be manually verified against the real application. OWASP ZAP offers alert filters that permanently hide known, deliberately accepted findings, plus a context configuration that precisely scopes authentication and target boundaries. Without this ongoing maintenance, both tool classes accumulate so many ignored notifications over months that real findings drown in the noise, a state commonly known as alert fatigue.
6. Integrating SAST into the CI pipeline: PHPStan security rules
PHPStan itself is primarily a type checker, but it becomes a security tool when combined with specialized rule sets such as phpstan/phpstan-security or custom rules that flag dangerous function calls like eval(), unserialize() without allowed_classes, or system() with dynamic arguments. For Magento projects it is worth adding a rule that detects direct SQL string concatenation outside the Zend Db Select object, since this pattern shows up especially often in legacy modules. The key is to raise the analysis level gradually so existing code does not immediately produce hundreds of findings.
Wired in as a quality gate, a failing PHPStan run blocks the merge before insecure code ever reaches the main branch, much earlier than any DAST scan, which can only run against a deployed environment. The combination of a baseline file and a strict gate for new violations keeps adoption practical even in mature codebases with thousands of existing files.
# phpstan.neon: security-relevant rules for a Magento 2 codebase
parameters:
level: 6
paths:
- app/code/Mironsoft
# Freeze existing findings, only fail the build on new violations
baselineFile: phpstan-baseline.neon
ignoreErrors:
# Known Magento interface gap, documented and accepted
- '#Call to an undefined method .*PageInterface::getData\(\)#'
includes:
- vendor/phpstan/phpstan-security/extension.neon
services:
-
class: Mironsoft\PhpStanRules\NoRawSqlConcatenationRule
tags:
- phpstan.rules.rule
-
class: Mironsoft\PhpStanRules\NoUnsafeUnserializeRule
tags:
- phpstan.rules.rule
# .github/workflows/sast.yml: run static analysis as a required check
name: SAST
on:
pull_request:
branches: [main]
jobs:
phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
tools: composer:v2
- name: Install dependencies
run: composer install --no-progress --prefer-dist
# Fail the pipeline on any error above the current baseline
- name: Run PHPStan security analysis
run: vendor/bin/phpstan analyse app/code/Mironsoft --level=6 --error-format=github
- name: Run Psalm taint analysis
run: vendor/bin/psalm --taint-analysis --report=psalm-report.sarif
7. Psalm taint analysis: tracking data flow from source to sink
Psalm's taint analysis engine takes a more explicit approach than PHPStan: instead of just checking types, Psalm models the complete data flow between defined sources and sinks. Sources include superglobals, request objects, and database results; sinks are dangerous functions like exec(), echo in an HTML context, or PDO::query(). Once taint analysis is enabled, Psalm reports concrete finding types such as TaintedSql for SQL injection risk or TaintedHtml for reflected XSS, each with a complete call path from the entry point to the sink.
The advantage over generic rules lies in traceability: a developer does not just see that a function is unsafe, but the exact chain of method calls through which the unsafe input got there. That makes it easier both to judge whether a finding is real and to fix it precisely. Custom sanitization functions can be marked as trusted through @psalm-taint-escape annotations, so the false positive rate drops with every maintained annotation.
<?xml version="1.0"?>
<!-- psalm.xml: enable taint analysis for the whole codebase -->
<psalm
errorLevel="4"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="app/code/Mironsoft" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<!-- Run with: vendor/bin/psalm --taint-analysis --
Findings surface as TaintedSql, TaintedHtml, TaintedShell, etc. -->
<issueHandlers>
<TaintedSql errorLevel="error" />
<TaintedHtml errorLevel="error" />
<TaintedShell errorLevel="error" />
<TaintedUnserialize errorLevel="error" />
</issueHandlers>
</psalm>
8. DAST against a running staging environment
A DAST scan should never target production, only a staging environment that resembles the live system as closely as possible, including realistic test data but never real customer data. OWASP ZAP can be run as a CLI inside a Docker container and distinguishes between a fast baseline scan, which only passively observes traffic, and a full active scan, which fires payloads against every discovered parameter and generates significantly more time and load on the target environment. For authenticated areas, a context file with a login sequence and session recognition is required, otherwise ZAP only scans the publicly reachable pages.
An active scan should run outside core working hours, since it noticeably loads the staging environment and in the worst case alters test data. Results are written as an HTML or JSON report, ideally automated as a CI pipeline artifact, so findings do not get lost in a separate console but land directly in the usual review process.
#!/usr/bin/env bash
# run-dast-scan.sh: run an OWASP ZAP scan against the staging environment
set -euo pipefail
readonly STAGING_URL="https://staging.mironsoft.de"
readonly CONTEXT_FILE="zap/mironsoft-staging.context"
readonly REPORT_DIR="zap-reports/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$REPORT_DIR"
# Fast baseline scan: passive checks only, safe to run on every deploy
docker run --rm -v "$(pwd)/${REPORT_DIR}:/zap/wrk:rw" \
ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t "$STAGING_URL" \
-r baseline-report.html \
-J baseline-report.json
# Full active scan: authenticated, off-hours only, higher load
docker run --rm \
-v "$(pwd)/${CONTEXT_FILE}:/zap/context.context:ro" \
-v "$(pwd)/${REPORT_DIR}:/zap/wrk:rw" \
ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
-t "$STAGING_URL" \
-n /zap/context.context \
-r active-report.html \
-J active-report.json \
-z "-config api.disablekey=true"
echo "[OK] Reports written to ${REPORT_DIR}"
9. SAST and DAST head to head
Both tool classes cost time, infrastructure, and maintenance effort, but at different points in the development cycle. SAST is almost always worth adopting first, because the barrier to entry is low: no running environment needed, results within seconds, direct integration into every pull request. DAST, by contrast, requires a stable staging environment, test data, an authentication setup, and time for the scan to run, but in return it delivers statements about the system as a whole that no code scanner can make.
| Dimension | SAST | DAST |
|---|---|---|
| Detection scope | Entire source code, including unreached paths | Only endpoints actually reachable at runtime |
| False positive rate | High without a baseline and tuning | Lower, since real server responses are verified |
| Earliest CI integration point | Pre-commit or pre-merge | Only after deployment to staging |
| Runtime per pass | Seconds to a few minutes | Minutes to hours depending on scan scope |
| Typical vulnerability classes | Taint flows, SQLi/XSS in code, unsafe deserialization | Auth bypass, server misconfiguration, session handling |
| Business logic flaws | Barely detectable without contextual knowledge | Only with deliberately written test scenarios |
Teams with budget for only one tool should start with SAST, because it stops vulnerabilities before the merge and is the cheapest per issue found. DAST becomes mandatory at the latest once an application is publicly reachable and compliance requirements such as PCI DSS call for regular external scans. The most economical strategy is rarely "either or", but rather SAST on every commit and DAST on a fixed cadence, for example weekly or before every major release.
Mironsoft
Application security testing for Magento and Hyva projects
Want SAST and DAST established in your Magento pipeline?
We set up PHPStan security rules and Psalm taint analysis in your CI pipeline, configure OWASP ZAP scans against your staging environment, and help keep false positives permanently under control.
Security audit
PHPStan and Psalm configuration with security rules for your codebase
CI integration
Set up taint analysis and quality gates in GitLab CI or GitHub Actions
DAST scans
Plan OWASP ZAP scans against staging environments and triage the results
10. Summary
SAST and DAST tools solve complementary, not competing, tasks. SAST analyzes the source code without executing it and finds taint flows, unsafe function calls, and data leaks before code is even merged, but it does not cover runtime configuration. DAST tests the running application like a real attacker and finds server misconfigurations, auth bypasses, and session issues, but it misses code paths the crawler never reaches. PHPStan security rules and Psalm taint analysis belong in every CI pipeline, because they are fast, cheap, and catch problems early in the development process.
OWASP ZAP against a realistic staging environment complements this early testing with an outside-in perspective, especially for misconfigurations that can never be derived from source code alone. The decisive success factor is consistent tuning on both sides: baselines and sanitization annotations for SAST, alert filters and precise context configuration for DAST, so real findings do not drown in a sea of false positives.
SAST and DAST Tools Compared: the essentials at a glance
SAST: source code analysis
Catches taint flows and unsafe function calls before the merge. PHPStan and Psalm are the standard tools for PHP.
DAST: runtime analysis
Uncovers real attack surfaces and misconfigurations on staging. OWASP ZAP simulates real HTTP attacks.
Keeping false positives in check
Baselines, sanitization annotations, and alert filters noticeably reduce the noise level on both sides.
Combined strategy
SAST on every commit, DAST regularly against staging. Both approaches complement each other, neither replaces the other.