Context
Ran semgrep scan --pro --config=auto (Semgrep Pro 1.157.0) on the full codebase. Documenting the triage so future scans can skip re-validating the same false positives.
Scan parameters:
semgrep scan --pro --config=auto \
--exclude=node_modules --exclude=vendor \
--exclude='*/vendor/*' \
--exclude=public/assets/tinymce --exclude=public/assets/uppy \
--exclude=public/assets/sweetalert2 \
--exclude='*.min.js' --exclude='*.min.css'
Result: 468 rules, 582 files, 117 findings (117 blocking) — 0 real vulnerabilities after triage.
Summary by severity
| Severity |
Count |
Real |
False Positive |
| ERROR |
14 |
0 |
14 |
| WARNING |
101 |
~3 (minor hardening) |
~98 |
| INFO |
2 |
0 |
2 |
ERROR breakdown — all 14 false positives
| Rule |
Count |
Files |
Why FP |
generic.secrets.detected-stripe-api-key |
3 |
storage/plugins/api-book-scraper/README.md, SERVER_IMPLEMENTATION_GUIDE.md |
Example placeholder sk_live_9f8e7d6c5b4a3210fedcba9876543210 in documentation — not a real key |
generic.secrets.detected-picatic-api-key |
3 |
same files |
Same placeholder, different rule match |
generic.secrets.detected-bcrypt-hash |
1 |
app/Controllers/AuthController.php:70 |
Dummy hash intentional — documented inline as OWASP timing-attack defense for the email-enumeration mitigation; not a leaked credential |
php.lang.security.taint-cookie-http-false |
2 |
app/Support/RememberMeService.php:330,345 |
Code literally sets 'httponly' => true. Rule misreads the array literal |
php.lang.security.taint-cookie-secure-false |
2 |
app/Support/RememberMeService.php:330,345 |
'secure' => $this->isSecureConnection() — conditional that's true on HTTPS, correct pattern. Rule can't statically resolve the call |
php.lang.security.exec-use.exec-use |
3 |
installer/classes/Installer.php:323,457, scripts/manual-upgrade.php:459 |
All exec() calls escape every variable argument with escapeshellarg(). Used at install/upgrade time only, mysql CLI bootstrap |
WARNING breakdown
php.lang.security.unlink-use.unlink-use — 58 findings — mostly FP
The path argument is internally resolved (via realpath() + containment check on the upload root) before every unlink() callsite. Specifically:
| File |
Count |
Status |
app/Support/Updater.php |
17 |
Internal storage/updates/* cleanup — paths come from glob() over a fixed directory |
app/Support/QueryCache.php |
9 |
Cache file invalidation — paths come from glob() over storage/cache/queries |
scripts/manual-upgrade.php |
7 |
Install-time scripts, run by operator |
storage/plugins/archives/ArchivesPlugin.php |
5 |
Cover/document upload cleanup — all paths run through realpath + base-dir containment |
| Other (controllers, plugins) |
20 |
Same pattern: internal-resolved paths, no direct user input |
app/Controllers/EventsController.php:1 is the canonical hardened pattern — see the deleteUploadedImageFile() helper for the prefix-check + realpath containment guard introduced in PR #144 (F00 fix).
php.symfony.security.audit.symfony-non-literal-redirect — 21 findings — all FP
All 21 are in app/Controllers/SettingsController.php. Every flagged site is $this->redirect($response, '/admin/settings?tab=<literal>') — a complete string literal in the source. The rule misclassifies any redirect() argument as "non-literal" because the helper's parameter is typed string (no statically-pinned constant). Tracked in #145 for the broader admin-route i18n decision.
php.lang.security.audit.openssl-decrypt-validate — 4 findings — all FP
| File |
Line |
Status |
app/Support/Updater.php |
293 |
Code already does $decrypted !== false ? $decrypted : null — exactly what the rule asks for |
storage/plugins/api-book-scraper/ApiBookScraperPlugin.php |
275 |
Same pattern |
storage/plugins/z39-server/Z39ServerPlugin.php |
915 |
Same pattern |
storage/plugins/z39-server/endpoint.php |
222 |
Same pattern |
php.lang.security.unserialize-use — 2 findings — all FP
app/Support/QueryCache.php:366,458 — both call unserialize($content, ['allowed_classes' => false]). The allowed_classes => false option is the documented mitigation (PHP prevents class instantiation → no object-injection RCE).
javascript.lang.security.audit.path-traversal.path-join-resolve-traversal — 4 findings — all FP
scripts/copy-vendor-assets.js:25-26 — build-time script that copies bundled vendor assets from node_modules to public/assets. No user input — paths come from a hardcoded vendor list. Runs only on npm run build.
javascript.lang.security.audit.detect-non-literal-regexp — 5 findings
public/assets/661.bundle.js (bundled JS — third-party libs) + dev tooling. Most likely FP from minified output. Low priority.
php.lang.security.injection.tainted-filename — 4 findings
Need case-by-case review (not done yet — TBD).
php.lang.security.taint-unsafe-echo-tag — 3 findings — hardened in commit c06fab01
All 3 sites use a map-lookup-with-fallback pattern: echo $errorMap[$_GET['key']] ?? __('default'). The map values are i18n-controlled, the fallback is a fixed literal — so the practical XSS is already gated by the allow-list. But the commit c06fab01 adds htmlspecialchars(…, ENT_QUOTES, 'UTF-8') on output anyway so future contributors adding free-form messages can't introduce XSS, and the rule stays quiet.
Files: app/Views/frontend/book-detail.php:1756, app/Views/profile/index.php:332, app/Views/prestiti/index.php:73.
Action items
How to re-run
semgrep scan --pro --config=auto \
--exclude=node_modules --exclude=vendor \
--exclude='*/vendor/*' \
--exclude=public/assets/tinymce --exclude=public/assets/uppy \
--exclude=public/assets/sweetalert2 \
--exclude='*.min.js' --exclude='*.min.css' \
--json-output=/tmp/semgrep-pinakes.json \
.
Related
Context
Ran
semgrep scan --pro --config=auto(Semgrep Pro 1.157.0) on the full codebase. Documenting the triage so future scans can skip re-validating the same false positives.Scan parameters:
Result: 468 rules, 582 files, 117 findings (117 blocking) — 0 real vulnerabilities after triage.
Summary by severity
ERROR breakdown — all 14 false positives
generic.secrets.detected-stripe-api-keystorage/plugins/api-book-scraper/README.md,SERVER_IMPLEMENTATION_GUIDE.mdsk_live_9f8e7d6c5b4a3210fedcba9876543210in documentation — not a real keygeneric.secrets.detected-picatic-api-keygeneric.secrets.detected-bcrypt-hashapp/Controllers/AuthController.php:70php.lang.security.taint-cookie-http-falseapp/Support/RememberMeService.php:330,345'httponly' => true. Rule misreads the array literalphp.lang.security.taint-cookie-secure-falseapp/Support/RememberMeService.php:330,345'secure' => $this->isSecureConnection()— conditional that's true on HTTPS, correct pattern. Rule can't statically resolve the callphp.lang.security.exec-use.exec-useinstaller/classes/Installer.php:323,457,scripts/manual-upgrade.php:459exec()calls escape every variable argument withescapeshellarg(). Used at install/upgrade time only, mysql CLI bootstrapWARNING breakdown
php.lang.security.unlink-use.unlink-use— 58 findings — mostly FPThe path argument is internally resolved (via
realpath()+ containment check on the upload root) before everyunlink()callsite. Specifically:app/Support/Updater.phpstorage/updates/*cleanup — paths come fromglob()over a fixed directoryapp/Support/QueryCache.phpglob()overstorage/cache/queriesscripts/manual-upgrade.phpstorage/plugins/archives/ArchivesPlugin.phprealpath+ base-dir containmentapp/Controllers/EventsController.php:1is the canonical hardened pattern — see thedeleteUploadedImageFile()helper for the prefix-check + realpath containment guard introduced in PR #144 (F00 fix).php.symfony.security.audit.symfony-non-literal-redirect— 21 findings — all FPAll 21 are in
app/Controllers/SettingsController.php. Every flagged site is$this->redirect($response, '/admin/settings?tab=<literal>')— a complete string literal in the source. The rule misclassifies anyredirect()argument as "non-literal" because the helper's parameter is typedstring(no statically-pinned constant). Tracked in #145 for the broader admin-route i18n decision.php.lang.security.audit.openssl-decrypt-validate— 4 findings — all FPapp/Support/Updater.php$decrypted !== false ? $decrypted : null— exactly what the rule asks forstorage/plugins/api-book-scraper/ApiBookScraperPlugin.phpstorage/plugins/z39-server/Z39ServerPlugin.phpstorage/plugins/z39-server/endpoint.phpphp.lang.security.unserialize-use— 2 findings — all FPapp/Support/QueryCache.php:366,458— both callunserialize($content, ['allowed_classes' => false]). Theallowed_classes => falseoption is the documented mitigation (PHP prevents class instantiation → no object-injection RCE).javascript.lang.security.audit.path-traversal.path-join-resolve-traversal— 4 findings — all FPscripts/copy-vendor-assets.js:25-26— build-time script that copies bundled vendor assets fromnode_modulestopublic/assets. No user input — paths come from a hardcoded vendor list. Runs only onnpm run build.javascript.lang.security.audit.detect-non-literal-regexp— 5 findingspublic/assets/661.bundle.js(bundled JS — third-party libs) + dev tooling. Most likely FP from minified output. Low priority.php.lang.security.injection.tainted-filename— 4 findingsNeed case-by-case review (not done yet — TBD).
php.lang.security.taint-unsafe-echo-tag— 3 findings — hardened in commitc06fab01All 3 sites use a map-lookup-with-fallback pattern:
echo $errorMap[$_GET['key']] ?? __('default'). The map values are i18n-controlled, the fallback is a fixed literal — so the practical XSS is already gated by the allow-list. But the commitc06fab01addshtmlspecialchars(…, ENT_QUOTES, 'UTF-8')on output anyway so future contributors adding free-form messages can't introduce XSS, and the rule stays quiet.Files:
app/Views/frontend/book-detail.php:1756,app/Views/profile/index.php:332,app/Views/prestiti/index.php:73.Action items
c06fab01on PR release: unified roadmap — SwalApp + event layout + RiC-CM (#136 + #139 + #141) #144.semgrepignoreor.semgrep.ymlconfig to silence the documented FPs so future scans surface only NEW signal. Specifically:storage/plugins/api-book-scraper/README.md,SERVER_IMPLEMENTATION_GUIDE.md(docs with placeholder credentials)php.symfony.security.audit.symfony-non-literal-redirect(over-aggressive againstredirect()calls)php.lang.security.audit.openssl-decrypt-validate(project already has the!== falsecheck)// nosemgrep: rule-idline above each known FP so the rule keeps firing globally but stays quiet on the validated sites.tainted-filenamefindings and the 5detect-non-literal-regexpfindings on a future pass.How to re-run
Related
/admin/*translatable routes — 385 hardcoded sites #145 (admin route hardcoding — explains the 21 non-literal-redirect FPs)c06fab01(3 unsafe-echo-tag hardenings)