200 lines
13 KiB
JSON
200 lines
13 KiB
JSON
{
|
|
"summary": {
|
|
"assessment_date": "2026-06-04",
|
|
"reviewer": "OpenAI GPT-5.5 Thinking",
|
|
"application": "school_api",
|
|
"framework_observed": "Laravel 12 (not CodeIgniter 4)",
|
|
"total_findings": 12,
|
|
"fixed_findings": 9,
|
|
"open_findings": 3,
|
|
"overall_risk_level": "Medium after static fixes; High until real secrets are rotated and dynamic testing/dependency audit are completed",
|
|
"verification_commands": [
|
|
"php -l changed PHP files: passed",
|
|
"composer audit: blocked - composer command not found",
|
|
"php artisan route:list: blocked - missing PHP mbstring extension (mb_split undefined)",
|
|
"static secret scan: original app/JWT/SMTP/PayPal secrets removed from edited .env/test.sql"
|
|
]
|
|
},
|
|
"findings": [
|
|
{
|
|
"id": "SEC-001",
|
|
"severity": "Critical",
|
|
"category": "Secrets / Environment",
|
|
"location": ".env; test.sql",
|
|
"status": "Fixed",
|
|
"finding": "The archive shipped active-looking application, JWT, SMTP, database and PayPal credentials, plus production debug/local settings.",
|
|
"risk": "Credential exposure can enable account takeover, forged JWTs, SMTP abuse, payment-provider abuse, and easier exploitation through debug traces.",
|
|
"evidence": "Static secret scan found APP_KEY, JWT_SECRET, Gmail app password, DB root/root, and PayPal client/secret values.",
|
|
"remediation": "Redacted shipped secrets to placeholders, changed production posture, enabled encrypted/secure session settings, and redacted PayPal credentials in test.sql.",
|
|
"before": "APP_DEBUG=true; MAIL_DEFAULT_PASS contained a Gmail app password; JWT_SECRET contained a live-looking HMAC secret; test.sql included PayPal secrets.",
|
|
"after": "APP_DEBUG=false; secrets use CHANGE_ME placeholders; SESSION_ENCRYPT=true; SESSION_SECURE_COOKIE=true; PayPal values redacted.",
|
|
"verification": "Repeated static secret scan. Remaining matches are placeholders or framework config keys; no original Gmail/JWT/PayPal strings remained.",
|
|
"result": "Fixed in source archive. Real production credentials must still be rotated outside this codebase."
|
|
},
|
|
{
|
|
"id": "SEC-002",
|
|
"severity": "High",
|
|
"category": "CSRF / Session State Change",
|
|
"location": "routes/web.php logout route",
|
|
"status": "Fixed",
|
|
"finding": "Browser-session logout used GET, a state-changing action vulnerable to CSRF-style forced logout and link/preload triggering.",
|
|
"risk": "An attacker could cause authenticated users to lose sessions through an image/link/preload request. Not catastrophic, but still sloppy. The web needed one less foot-gun.",
|
|
"evidence": "Route::get('logout', ...) was registered under the web middleware group.",
|
|
"remediation": "Changed logout to POST so Laravel web CSRF protection applies.",
|
|
"before": "Route::get('logout', [AuthSessionController::class, 'logout'])",
|
|
"after": "Route::post('logout', [AuthSessionController::class, 'logout'])",
|
|
"verification": "PHP lint passed for routes/web.php; diff confirms GET was removed for the logout route.",
|
|
"result": "Fixed. Frontend logout calls must use POST."
|
|
},
|
|
{
|
|
"id": "SEC-003",
|
|
"severity": "High",
|
|
"category": "Brute Force / Abuse Controls",
|
|
"location": "routes/api.php public auth, registration, invite, contact routes",
|
|
"status": "Fixed",
|
|
"finding": "Several public mutation endpoints had no route-level throttle: login/register aliases, captcha, contact, and authorized-user password setup.",
|
|
"risk": "Attackers could automate credential stuffing, registration abuse, contact spam, and invite/password token guessing at higher volume than necessary.",
|
|
"evidence": "Public POST/GET routes were registered without throttle middleware.",
|
|
"remediation": "Added route-level throttles: login 10/min, register 5/min, captcha 30/min, invite GET 20/min, password setup POST 10/min, contact 5/min.",
|
|
"before": "Route::post('login', ...); Route::post('register', ...); Route::post('contact', ...);",
|
|
"after": "Routes now include middleware('throttle:...') on public auth/contact/invite mutation paths.",
|
|
"verification": "PHP lint passed for routes/api.php; route snippets confirm throttle middleware is attached.",
|
|
"result": "Fixed statically. Live rate-limit behavior still needs HTTP retesting after deployment."
|
|
},
|
|
{
|
|
"id": "SEC-004",
|
|
"severity": "High",
|
|
"category": "File Upload Security",
|
|
"location": "app/Services/Events/EventManagementService.php",
|
|
"status": "Fixed",
|
|
"finding": "Event flyer upload moved files into public storage without explicit MIME allowlist or file-size enforcement in the service.",
|
|
"risk": "Untrusted files could be stored in a web-accessible directory. Extension games are ancient, stupid, and still work when services trust uploads too much.",
|
|
"evidence": "storeFlyer() used hashName() and move() without validating MIME or size inside the service.",
|
|
"remediation": "Added service-level MIME allowlist for JPEG, PNG, WEBP, PDF; max 5MB; cryptographically random server-side filename using the validated MIME extension.",
|
|
"before": "$name = $file->hashName(); $file->move($dir, $name);",
|
|
"after": "MIME map validation, size check, random_bytes filename, controlled extension.",
|
|
"verification": "PHP lint passed for EventManagementService.php; static diff confirms validation was added before move().",
|
|
"result": "Fixed."
|
|
},
|
|
{
|
|
"id": "SEC-005",
|
|
"severity": "High",
|
|
"category": "File Upload Security",
|
|
"location": "app/Services/Expenses/ExpenseReceiptService.php",
|
|
"status": "Fixed",
|
|
"finding": "Receipt upload stored files without service-level validation of validity, MIME type, or size.",
|
|
"risk": "Malformed or unexpected files could be accepted and later served/downloaded, increasing malware and content-sniffing risk.",
|
|
"evidence": "storeReceipt() directly called $file->store('receipts') and returned basename.",
|
|
"remediation": "Added isValid(), MIME allowlist, max 5MB, random server-side filename, and controlled extension.",
|
|
"before": "$stored = $file->store('receipts');",
|
|
"after": "Validated MIME and size, then storeAs() with bin2hex(random_bytes(16)).",
|
|
"verification": "PHP lint passed for ExpenseReceiptService.php; static diff confirms checks and randomized naming.",
|
|
"result": "Fixed."
|
|
},
|
|
{
|
|
"id": "SEC-006",
|
|
"severity": "Medium",
|
|
"category": "File Upload Security",
|
|
"location": "app/Services/BroadcastEmail/BroadcastEmailImageService.php",
|
|
"status": "Fixed",
|
|
"finding": "Broadcast email image upload used uniqid() for public filenames and lacked an explicit isValid() check.",
|
|
"risk": "uniqid() is predictable enough to make public file discovery easier. Tiny risk, but still a classic example of software pretending timestamps are secrets.",
|
|
"evidence": "$newName = uniqid('em_', true) . '.' . $ext; no isValid() guard.",
|
|
"remediation": "Added isValid() handling and replaced uniqid() with bin2hex(random_bytes(16)).",
|
|
"before": "uniqid('em_', true)",
|
|
"after": "'em_' . bin2hex(random_bytes(16))",
|
|
"verification": "PHP lint passed for BroadcastEmailImageService.php; diff confirms change.",
|
|
"result": "Fixed."
|
|
},
|
|
{
|
|
"id": "SEC-007",
|
|
"severity": "Medium",
|
|
"category": "File Upload Security",
|
|
"location": "PrintRequestsController; PrintRequestsPortalService",
|
|
"status": "Fixed",
|
|
"finding": "Print request uploads had controller extension/MIME validation but service naming still trusted the client original extension.",
|
|
"risk": "Client-controlled extensions can create misleading stored files and weaken downstream serving assumptions.",
|
|
"evidence": "Portal service used getClientOriginalExtension() to build stored filenames.",
|
|
"remediation": "Added safeExtensionForUpload() that maps detected MIME to controlled extensions; added mimetypes rules to controller validation.",
|
|
"before": "$ext = $file->getClientOriginalExtension();",
|
|
"after": "$ext = $this->safeExtensionForUpload($file); plus mimetypes validation.",
|
|
"verification": "PHP lint passed for controller and service; diff confirms service no longer trusts client extensions.",
|
|
"result": "Fixed."
|
|
},
|
|
{
|
|
"id": "SEC-008",
|
|
"severity": "Medium",
|
|
"category": "HTTP Security Headers / Content Sniffing",
|
|
"location": "bootstrap/app.php; app/Http/Middleware/SecurityHeaders.php; FileServeService.php",
|
|
"status": "Fixed",
|
|
"finding": "The app lacked a global security header middleware and some uploaded/private file responses were cacheable as public unless nosniff was manually requested.",
|
|
"risk": "Missing headers increases clickjacking/content-sniffing exposure. Public caching of private uploaded files can leak sensitive school documents through shared caches.",
|
|
"evidence": "No global header middleware; FileServeService used Cache-Control public, max-age=86400 and conditional nosniff.",
|
|
"remediation": "Added global SecurityHeaders middleware; added nosniff, frame denial, referrer policy, permissions policy, HSTS over HTTPS; changed file serving to private max-age=300 and nosniff always.",
|
|
"before": "Cache-Control: public, max-age=86400; conditional X-Content-Type-Options.",
|
|
"after": "Cache-Control: private, max-age=300; X-Content-Type-Options: nosniff; global headers middleware.",
|
|
"verification": "PHP lint passed for middleware, bootstrap, and FileServeService; static diff confirms middleware registration.",
|
|
"result": "Fixed statically. Header behavior should be confirmed by HTTP response tests in deployed environment."
|
|
},
|
|
{
|
|
"id": "SEC-009",
|
|
"severity": "Medium",
|
|
"category": "SQL Injection Hardening",
|
|
"location": "app/Models/User.php",
|
|
"status": "Fixed",
|
|
"finding": "A school_year value was manually quoted and interpolated into a raw EXISTS SQL string.",
|
|
"risk": "Manual quoting is fragile. It was not obviously exploitable because PDO quote was used, but parameterized query builder logic is safer and auditable.",
|
|
"evidence": "$escYear = DB::getPdo()->quote($schoolYear); ... tc.school_year = {$escYear}",
|
|
"remediation": "Replaced raw interpolation with whereExists(), whereColumn(), and bound where('tc.school_year', $schoolYear).",
|
|
"before": "tc.school_year = {$escYear}",
|
|
"after": "->where('tc.school_year', $schoolYear)",
|
|
"verification": "PHP lint passed for User.php; static diff confirms interpolation removed.",
|
|
"result": "Fixed."
|
|
},
|
|
{
|
|
"id": "SEC-010",
|
|
"severity": "Medium",
|
|
"category": "Dependency Audit",
|
|
"location": "composer.lock / environment",
|
|
"status": "Open",
|
|
"finding": "Dependency audit could not be completed in this sandbox because composer is not installed.",
|
|
"risk": "Known vulnerable packages may remain. Laravel, JWT, PDF, and mail libraries should be audited before deployment because humans keep shipping libraries faster than they patch them.",
|
|
"evidence": "composer audit returned: bash: composer: command not found.",
|
|
"remediation": "Not fixed here. Run composer audit in CI and fail builds on high/critical advisories.",
|
|
"before": "No verified dependency advisory baseline from this run.",
|
|
"after": "Open remediation item documented.",
|
|
"verification": "Attempted composer audit; blocked by missing composer binary.",
|
|
"result": "Open."
|
|
},
|
|
{
|
|
"id": "SEC-011",
|
|
"severity": "Medium",
|
|
"category": "Dynamic Security Testing",
|
|
"location": "Running application / OWASP ZAP",
|
|
"status": "Open",
|
|
"finding": "OWASP ZAP and manual HTTP abuse testing were not executed because no running target with required PHP extensions/database was available in the sandbox.",
|
|
"risk": "Static review can miss runtime authorization, CORS, CSRF token, redirect, and response-header issues.",
|
|
"evidence": "php artisan route:list failed because the PHP runtime lacks mbstring: undefined function mb_split().",
|
|
"remediation": "Install required PHP extensions, configure a test database, boot the app, then run authenticated and unauthenticated ZAP baseline plus manual endpoint abuse tests.",
|
|
"before": "No dynamic scan baseline.",
|
|
"after": "Open remediation item documented with blocker.",
|
|
"verification": "Artisan route listing attempted; blocked by missing mbstring extension.",
|
|
"result": "Open."
|
|
},
|
|
{
|
|
"id": "SEC-012",
|
|
"severity": "Low",
|
|
"category": "Framework Scope Accuracy",
|
|
"location": "security-verification-plan.md",
|
|
"status": "Open",
|
|
"finding": "The uploaded plan says CodeIgniter 4, but the application is Laravel 12.",
|
|
"risk": "Wrong-framework checklists miss framework-specific controls. This is how audits become theater with better fonts.",
|
|
"evidence": "composer.json name/type and dependencies identify laravel/framework ^12.0; routes, middleware, FormRequests, and artisan confirm Laravel structure.",
|
|
"remediation": "Documented scope mismatch. Future audits should use a Laravel-specific checklist.",
|
|
"before": "CodeIgniter 4 plan wording.",
|
|
"after": "This report applies the plan categories to Laravel controls.",
|
|
"verification": "Static framework identification completed from composer.json and project structure.",
|
|
"result": "Open documentation correction."
|
|
}
|
|
]
|
|
} |