Skip to content
prod e051e98
Browse

2 · Security audit

Objective — run the full security pass (headers, dependency vulnerabilities with ownership-aware decisions, exposed files, admin hardening, storage permissions) so no known-controllable risk ships to production.

The full security pass: headers, dependency vulnerabilities with ownership-aware decisions, exposed files, admin hardening, and storage permissions.

The CodeCanyon twist is that you don’t own most of the dependency tree. Decide each vulnerability by who controls the package, not just by severity.

CategoryWho controls itUpdate?Conflict risk
Vendor core / dependenciesCodeCanyon authorRiskyHIGH
Your additions (Sentry, Spatie…)YouSafeNONE
Shared packagesBothCheck firstMEDIUM
flowchart TD
CVE[composer audit finding] --> Q{Who owns the package?}
Q -->|Vendor core| Risky[Defer / vendor update path]
Q -->|Your addition| Safe[Patch or bump freely]
Q -->|Shared| Check[Review conflict risk first]

Aim for Grade A at securityheaders.com (pragmatic CSP caps at A until nonce/hash hardening) and SSL Labs (TLS) A/A+, or check the six headers from the CLI.

  1. Grade the six required headers from the CLI (or a public scanner).

    Terminal window
    curl -I https://<YOUR_DOMAIN> 2>/dev/null | \
    grep -iE "strict-transport|x-frame|x-content-type|referrer-policy|content-security|permissions-policy"
    # Expected: all six security headers present
    • ✅ All six are present and match the canonical set defined in Phase 7 · Security headers (this phase verifies, it does not redefine): Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Content-Security-Policy, Permissions-Policy. The CSP must carry real directives (default-src 'self' + frame-ancestors/base-uri/form-action), not just upgrade-insecure-requests — a bare upgrade directive grades well while giving zero XSS protection.

Every path below must return 403/404, never 200.

  1. Request each sensitive path and inspect status, content type, and body. A status-only check can false-pass when the host returns a branded HTML page, a PHP warning page, or a downloaded text body with a non-200 status.

    Terminal window
    for path in ".env" ".git/config" "composer.json" "storage/logs/laravel.log" "vendor/composer/installed.json"; do
    printf "\n== %s ==\n" "$path"
    body="/tmp/security-path-${path//\//-}.body"
    curl -sk -D - "https://<YOUR_DOMAIN>/$path" -o "$body" \
    | awk 'BEGIN{IGNORECASE=1} /^HTTP|^content-type|^content-length/ {print}'
    head -c 240 "$body" | sed 's/[[:cntrl:]]/ /g'
    done
    # Expected: each path returns 403/404, non-secret body, and no PHP/JSON/env content
    • ✅ Every path returns 403/404; none returns 200; no response body exposes .env, Composer metadata, logs, PHP warnings, or JSON package details.
  2. If you rely on .htaccess (lowest-priority method), verify it actually carries the headers and a sensitive-file deny block. Prefer Cloudflare/nginx (see the caution below); .htaccess is the fallback a vendor update can overwrite.

    Terminal window
    HT="public/.htaccess"
    [ -f "$HT" ] && echo "OK $HT exists" || echo "MISSING $HT"
    for needle in "RewriteEngine On" "Strict-Transport-Security" "X-Frame-Options" \
    "X-Content-Type-Options" "Referrer-Policy" "Permissions-Policy"; do
    grep -q "$needle" "$HT" 2>/dev/null && echo "OK $needle" || echo "WARN missing: $needle"
    done
    • .htaccess carries the six headers (or edge/nginx owns them instead).
  3. Verify server-level denial and headers are actually configured. Edge headers are preferred, but shared hosting often still needs .htaccess guards for dotfiles, logs, composer files, and directory listing.

    Terminal window
    grep -nE "Header set|Header always set|FilesMatch|Require all denied|RedirectMatch|Options -Indexes" .htaccess public/.htaccess 2>/dev/null || true
    # Expected: security headers or edge-note present; sensitive-file deny rules present; directory listing disabled
    • .htaccess or the documented edge layer blocks sensitive files, disables directory indexes, and does not contradict the Phase 7 header strategy.

Then act by the table below — prefer mitigation over patching vendor code.

ActionWhenConflict risk
Update nowYour package, any severityNONE
Disable featureUnused vendor packageNONE
WAF ruleCRITICAL, can’t updateNONE
Accept & documentLow severity, vendor packageNONE
Patch vendor packageCRITICAL, no alternativeHIGH
  1. Determine ownership mechanically — diff against the vendor’s pristine manifest. The CodeCanyon author’s original composer.json is the source of truth for “vendor-owned vs. your addition”. Compare it against the current manifest (substitute your pristine baseline ref — a tag or branch like author/<VENDOR_TAG> captured when you first imported the item).

    Terminal window
    PKG="<vulnerable/package-name>" # from `composer audit`
    BASE="author/<VENDOR_TAG>" # your pristine vendor baseline (tag or branch)
    if git show "$BASE:composer.json" >/dev/null 2>&1; then
    if git show "$BASE:composer.json" | grep -q "\"$PKG\""; then
    echo "VENDOR-OWNED: $PKG is in the vendor baseline → HIGH conflict risk (prefer mitigation; see table)"
    else
    echo "YOUR ADDITION: $PKG is NOT in the vendor baseline → safe to update"
    fi
    else
    echo "SKIP: baseline ref '$BASE' not found — tag the pristine import first, then re-run"
    fi
    • ✅ Each finding is labelled VENDOR-OWNED or YOUR ADDITION by manifest diff (not by guesswork), and only then mapped to a row of the action table above.
  2. Assign each vulnerability an action by ownership and severity, preferring mitigation over patching vendor code.

    • ✅ Every finding maps to one row of the table above, with vendor patches reserved for CRITICAL-with-no-alternative.
  3. Prove ownership before editing a vulnerable package. If a package appears to be vendor-owned, check the source tree and the last vendor snapshot before changing it.

    Terminal window
    export VENDOR_TAG=<vendor-import-tag-or-commit>
    grep -R "<package-or-namespace>" composer.json composer.lock app/ Modules/ packages/ routes/ config/ --line-number
    git diff "$VENDOR_TAG"...HEAD -- composer.json composer.lock app/ Modules/ packages/ | sed -n '1,180p'
    # Expected: package owner is clear: vendor core, your addition, or shared
    • ✅ Every dependency decision records whether the package is vendor core, your addition, or shared; shared packages get a conflict-risk note before updating.

Lock down the admin account and the storage permissions before launch.

  1. Rotate the admin login, enable 2FA, and confirm rate-limiting + permissions. Default credentials changed, 2FA on if available, login rate-limiting confirmed, and storage/ + bootstrap/cache/ at 775.

    • ✅ Default admin credentials are changed, 2FA is on (if available), rate-limiting works, and storage/ + bootstrap/cache/ are at 775.
  2. Block sensitive files at the web server when .htaccess is your control plane (idempotent — only appended if absent).

    # Append to public/.htaccess if not already present:
    <FilesMatch "^\.env|\.git|composer\.(json|lock)">
    Require all denied
    </FilesMatch>
    Terminal window
    # Idempotent guard
    grep -q 'FilesMatch.*composer' public/.htaccess 2>/dev/null \
    && echo "OK sensitive-file deny block present" \
    || echo "ACTION: add the <FilesMatch> deny block above to public/.htaccess"
    • .htaccess carries a deny block for .env/.git/composer.* (or those controls live at the edge/nginx instead).

Owner, decision, and a 30-day review date for every finding.

  1. Write the register — one row per vulnerability with owner, decision, and a 30-day review date.

    • ✅ Every finding has an owner, a recorded decision, and a 30-day review date.

Do not mark this step done until every box below is checked.

  • 🤖 Headers graded — all six required headers present; Grade A at securityheaders.com; SSL Labs (TLS) A or A+ recorded.
  • 🤖 Sensitive files blocked.env, .git/config, composer.json, logs all return 403/404.
  • 🔀 Vulnerabilities decided — each finding mapped to an ownership-aware action.
  • 👤 Admin hardened — default creds changed, 2FA on, rate-limiting confirmed, storage at 775.
  • 🤖 Vulnerability register written — owner + decision + 30-day review date per finding.