Conversation
Wire a Checks / Rules job into PR CI and the scheduled security scan so common skip, token-response, outbound-map, header-filter, and nested permission patterns fail the build instead of waiting on review. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
Expand skip and token-response matching, drop screenshots path excludes in favor of WARNING review signal, and add client-IP and placeholder-secret checks. ERROR scan on current main stays clean. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Replace the userdata skip list with an allowlisted skip+load class, cover token/JWT output, related-permission name families, guest ROLE_GUESTS scopes, and guest URL params without PublicURL. Leave known screenshots sites as WARNING instead of path excludes. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
✨ Benchmark resultsComparing
Per-scenario breakdown & investigation detailsMetrics below reflect the current branch (after). Δ P95 compares against the base.
Top API waits (after)
|
…on PRs. Drop the named-collection ERROR leftover. Ungated skip+load of any non-allowlisted collection now fails CI; fixtures and prove.sh show a known-bad snippet fails and a gated skip does not. Checks / Rules upserts one sticky PR comment for WARNING (and ERROR) findings. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Rename `static- rules` to `static-rules`. GitHub job ids cannot contain spaces, which left Checks / Rules (and the rest of CI) unscheduled. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Security rulesNo new WARNING or ERROR findings from security rules. 33 existing findings tracked in
|
Replace the three-column table (160-char mid-sentence clip) with a bullet list of file:line, rule id, and the complete rule message. Drop whole findings only when the comment would exceed GitHub's size limit. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
client-ip-header covers every forwarding header and accessor; the placeholder rule matches a literal class; guest URL and Assoc rules match any URL-like / map param on ROLE_GUESTS scopes and honour class ALLOWED_* allowlists; the token gate covers module action() methods and Key in any position; header-blocklist-filter is ERROR now that main is clean. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Every /v1 route needs a scope label, non-public routes need the api hook group, redirect-target params need redirectValidator, and guest-scoped writes without an abuse limit are surfaced as WARNING. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Dynamic code, process execution, disabled TLS verification, superglobals, request-wide authorization disable, showSensitive outside event payloads, non-cryptographic randomness, libxml entity loading, interpolated SQL, non-httponly cookies, debug output, and request-derived filesystem paths. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Secret env defaults, non-constant-time secret comparisons, any/guests write permissions, and outbound clients that follow redirects. Each has live findings on main and is listed on the sticky PR comment. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
prove.sh runs the real ERROR scan on one known-bad snippet per ERROR rule, asserts the expected rule id, and checks a file of house patterns stays clean. README gains a full trigger matrix and rule-writing notes. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
EncryptionKey on main declares the public default as a PLACEHOLDER constant and compares against it; constant propagation made every use an ERROR. Comparisons, warning/exception messages and a PLACEHOLDER constant are now ok, while another class's PLACEHOLDER as a getEnv default is caught explicitly. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
…mment. The formatter reads each matched file from the checkout to resolve the HTTP method, path and scope (or hook, Class::method, init resource), then prints a per-rule What's wrong / How to fix built from the matched code, with a link to the scanned commit. Findings are grouped by rule so the full rule message appears once per group. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
…-rules-eb3f Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
…nd Scan Rules. Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
Co-authored-by: Jake Barnby <abnegate@users.noreply.github.com>
🔵 Tier A · Mergeable after minor fixes
Adds 28 custom Semgrep rules with PHP fixtures and scratch proofs for Appwrite security conventions. Integrates scans into PR and scheduled CI, with a checked-in findings baseline, SARIF uploads, and sticky PR comments for new findings.
Note Part of the diff was too large to review, so Hansi did not approve.
Fix with agent prompt### Issue 1
.semgrep/request-path-to-filesystem.yml:44-45
**Do not treat realpath alone as a traversal sanitizer**
`realpath()` resolves `..` and symlinks but does not restrict the result to the intended directory, so `file_get_contents(realpath($base . '/' . $request->getParam('path')))` escapes this rule even when it reads outside `$base`. Keep the value tainted until a directory-containment check is enforced, and add a fixture for unchecked `realpath()`.
### Issue 2
.github/workflows/semgrep-comment.js:31-33
**Avoid posting an all-clear when the scan never produced JSON**
The workflow runs this comment step with `always()`, so an installation, fixture, proof, or scan failure can reach this missing-file branch. Returning an empty findings list then overwrites the sticky comment with “No new WARNING or ERROR findings”; skip the update or explicitly report that the scan did not complete instead.
### Issue 3
.semgrep/baseline.js:29-33
**Include the matched route identity in baseline keys**
For multiline route findings, this stops after `$this ->setHttpMethod(...)`, before `setHttpPath()`; the transaction entries in `baseline.json` already demonstrate this. Replacing a baselined route with a different unrate-limited route at the same path and HTTP method is therefore silently baselined, contrary to the documented behavior that editing matched code creates a new finding; include the matched span or at least the route path in the key.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.📂 Walkthrough · 8
🔇 Filtered out · 1Findings Hansi considered but did not post.
Reviewed |
| - pattern: realpath(...) | ||
| - pattern: \realpath(...) |
There was a problem hiding this comment.
Do not treat realpath alone as a traversal sanitizer
realpath() resolves .. and symlinks but does not restrict the result to the intended directory, so file_get_contents(realpath($base . '/' . $request->getParam('path'))) escapes this rule even when it reads outside $base. Keep the value tainted until a directory-containment check is enforced, and add a fixture for unchecked realpath().
Prompt To Fix With AI
This is a comment left during a code review.
Path: .semgrep/request-path-to-filesystem.yml
Line: 44-45
Comment:
**Do not treat realpath alone as a traversal sanitizer**
`realpath()` resolves `..` and symlinks but does not restrict the result to the intended directory, so `file_get_contents(realpath($base . '/' . $request->getParam('path')))` escapes this rule even when it reads outside `$base`. Keep the value tainted until a directory-containment check is enforced, and add a fixture for unchecked `realpath()`.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.🟡 Minor · bug · Reply if this doesn't apply.
| if (!fs.existsSync(path)) { | ||
| core?.warning(`Semgrep JSON not found at ${path}`); | ||
| return []; |
There was a problem hiding this comment.
Avoid posting an all-clear when the scan never produced JSON
The workflow runs this comment step with always(), so an installation, fixture, proof, or scan failure can reach this missing-file branch. Returning an empty findings list then overwrites the sticky comment with “No new WARNING or ERROR findings”; skip the update or explicitly report that the scan did not complete instead.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/semgrep-comment.js
Line: 31-33
Comment:
**Avoid posting an all-clear when the scan never produced JSON**
The workflow runs this comment step with `always()`, so an installation, fixture, proof, or scan failure can reach this missing-file branch. Returning an empty findings list then overwrites the sticky comment with “No new WARNING or ERROR findings”; skip the update or explicitly report that the scan did not complete instead.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.🟡 Minor · error-handling · Reply if this doesn't apply.
| let text = (lines[start - 1] || '').slice(Math.max(0, (result.start?.col || 1) - 1)); | ||
| for (let line = start + 1; line <= Math.min(end, start + TEXT_LINES - 1) && text.trim().length < TEXT_MIN; line++) { | ||
| text += ' ' + (lines[line - 1] || ''); | ||
| } | ||
| return text.replace(/\s+/g, ' ').trim(); |
There was a problem hiding this comment.
Include the matched route identity in baseline keys
For multiline route findings, this stops after $this ->setHttpMethod(...), before setHttpPath(); the transaction entries in baseline.json already demonstrate this. Replacing a baselined route with a different unrate-limited route at the same path and HTTP method is therefore silently baselined, contrary to the documented behavior that editing matched code creates a new finding; include the matched span or at least the route path in the key.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .semgrep/baseline.js
Line: 29-33
Comment:
**Include the matched route identity in baseline keys**
For multiline route findings, this stops after `$this ->setHttpMethod(...)`, before `setHttpPath()`; the transaction entries in `baseline.json` already demonstrate this. Replacing a baselined route with a different unrate-limited route at the same path and HTTP method is therefore silently baselined, contrary to the documented behavior that editing matched code creates a new finding; include the matched span or at least the route path in the key.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.🟡 Minor · bug · Reply if this doesn't apply.
What does this PR do?
Adds Security rules under
.semgrep/for Appwrite PHP coding conventions and wires them into CI. Rules are written as general pattern classes with allowlists, not lists of files or collections. CI changes only; no product code changes.Checks / Rules(and the scheduledScan Rulesjob).<!-- semgrep-rules-comment -->) that is updated in place. Findings are grouped by rule. Each one has:file:linelink to the scanned commit;Class::method(), or the init resource;.semgrep/baseline.json) neither fail the job nor appear in detail on the comment. The comment shows them only as per-rule counts in a collapsed block. They are still written to SARIF.Baseline. Merging this PR does not change the result for other PRs.
.semgrep/baseline.jsonrecords the 33 findings on the current tree, which ismainmerged into this branch. Semgrep OSS only supports a commit-diff baseline, so.semgrep/baseline.jsfilters the JSON scan itself:checkprints totals, lists new findings and stale entries, and exits 1 on any new ERROR.updateregenerates the file.Entries are keyed on rule id, path, and the whitespace-collapsed matched text, so line shifts elsewhere in a file do not resurface them. The regeneration steps are in
.semgrep/README.md.ERROR classes (23). These have 0 findings on the current tree.
route-without-scope(every/v1route declares a scope),route-without-api-group(non-public routes run the sharedapihooks),redirect-param-without-validatorskip-ungated-load,global-authorization-disable,related-permissions-helperaccount-token-secret-scope-gate(now covers moduleaction()methods andKeyin any position),show-sensitive-outside-payload,insecure-cookie-flagsguest-url-without-publicurl(any URL-like param name,URL()orText()),unbounded-map-to-outbound(anyAssocparam name; a class-levelALLOWED_*constant counts as filtered),header-blocklist-filterclient-ip-header(all forwarding headers and accessors, including host and proto)default-secret-placeholder(a class of placeholder literals),insecure-randomtls-verification-disabledunsafe-dynamic-code,shell-exec-in-handler,superglobal-in-handler,debug-output-in-handler,raw-sql-interpolation,xml-external-entities,request-path-to-filesystem(taint from request getters orstringaction params)WARNING classes (5). These have 33 findings on the current tree, all in the baseline. A class can be promoted to ERROR once its entries are cleared.
guest-write-without-abuse-limitpermissive-write-permissionsecret-compare-timingweak-secret-env-defaultoutbound-follow-redirectsNotes for rule authors (also in
.semgrep/README.md):unserialize(...)and\unserialize(...).metavariable-regexso that negative conditions can still see the full chain.explainersin.github/workflows/semgrep-comment.js.Test Plan
semgrep test .semgreppasses 28/28. Every rule has a sibling.phpfixture withruleidandokcases.semgrep --validatereports 28 rules and 0 errors withbaseline.js/baseline.jsonpresent.bash .semgrep/prove.sh:PublicURL,redirectValidator,ALLOWED_HEADERS,getIP(),random_bytes, bound SQL,basename, httponly cookies, and event-payloadshowSensitive. A second clean file mirrorsEncryptionKey(aPLACEHOLDERconstant, comparisons against it, and aConsole::warningnaming it).src/Appwrite app/controllers app/init:baseline.js checkreports 33 baselined, 0 new ERROR, 0 new WARNING, and exits 0.Permission::write(Role::any())next to baselined lines gives 2 new WARNINGs (exit 0).eval($request->getParam(...))file gives 1 new ERROR (exit 1).Related PRs and Issues
Checklist