8.8 KiB
M013 Adversarial Security Assessment
Scope
Tested as requested:
- Input validation issues
- Authentication flaws
- Authorization issues
- API security
- File upload vulnerabilities
- Data exposure
Assessment style: hostile, exploit-oriented, evidence-first.
Confirmed Findings
1. Authenticated SSRF via hostname-based loopback bypass in job import preview
- Category: API security / input validation
- Component:
JobTrackerApi/Services/JobImport/JobImportService.cs - Endpoint:
POST /api/jobimport/preview - Risk: High
Vulnerability
JobImportService.TryValidateUrl(...) blocks literal loopback and private IPs, but it does not resolve hostnames before allowing the request. That means a hostname that resolves to a loopback/private address can bypass the protection.
The validator rejects:
http://127.0.0.1:5202/...http://[::1]:5202/...http://2130706433:5202/...
But it accepted hostnames resolving to loopback, including:
http://127.0.0.1.nip.io:5202/api/auth/confighttp://localhost.localdomain:5202/api/auth/config
Example exploit input
POST /api/jobimport/preview
Authorization: Bearer <valid local token>
Content-Type: application/json
{
"url": "http://127.0.0.1.nip.io:5202/api/auth/config"
}
Observed result:
- request was not rejected as local/private
- server fetched the internal endpoint
- response progressed to parser failure (
No JobPosting schema found), which is enough to prove the internal fetch happened
Why this matters
An authenticated attacker can use the server as an HTTP client against internal-only services or private network resources reachable from the API host. Depending on deployment, this can expose:
- internal admin/debug endpoints
- cloud metadata services
- internal service meshes
- localhost-only ports
- network topology and response behavior
Clear fix
- Resolve DNS before allowing the request.
- Reject any hostname whose resolved addresses are loopback, link-local, RFC1918 private, or otherwise internal.
- Re-resolve after redirects, or disable redirects entirely.
- Consider an allowlist of supported job domains instead of general outbound fetching.
- Log and rate-limit preview fetch attempts.
2. Subjectless signed local JWTs authenticate successfully and can disable owner scoping
- Category: Authentication flaws / authorization issues
- Components:
JobTrackerApi/Program.csData/JobTrackerContext.cs- several owner-scoped controllers relying on EF query filters
- Risk: High
Vulnerability
Local JWT validation accepts a correctly signed token without a required subject / nameidentifier claim.
Runtime proof:
- a signed local JWT with no
ClaimTypes.NameIdentifier/sub - but with valid issuer/audience/signature
- was accepted by the API
GET /api/auth/mereturned200
Observed response shape:
- provider:
external - id:
null - email echoed from token
At the same time, owner scoping in Data/JobTrackerContext.cs is defined like this:
.HasQueryFilter(x => CurrentUserId == null || x.OwnerUserId == CurrentUserId)
If CurrentUserId is null, the filter collapses to allow all rows for owner-scoped entities.
That is a dangerous composition:
- token is authenticated
- current user id is null
- owner filters disable themselves
- endpoints that rely on implicit owner filtering become potentially cross-tenant
Example exploit input
A signed HS256 JWT using the app signing key, but omitting the nameidentifier claim.
Payload example:
{
"iss": "JobTrackerApi",
"aud": "job-tracker-ui",
"nbf": 1775910345,
"exp": 1775913945,
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "ghost@example.com",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "ghost@example.com"
}
Observed runtime request:
GET /api/auth/me
Authorization: Bearer <signed token without subject>
Observed result:
200 OK- request treated as authenticated even though no user identity key existed for owner scoping
Why this matters
If an attacker can forge or obtain a valid local signing key, this flaw is not just “login bypass” — it can become a tenant-boundary bypass because owner filters stop applying.
This is especially serious in environments where:
- the dev signing key is reused
- a staging or preview environment leaks the local JWT key
- operational mistakes deploy development auth settings
Clear fix
- Require a non-empty subject / nameidentifier claim during local JWT validation.
- Reject authenticated requests whose token does not map to a concrete application user identity.
- Change query filters so
CurrentUserId == nullmeans deny, not allow-all, for owner-scoped entities. - Avoid relying on implicit global filters alone for sensitive raw-id endpoints; add explicit owner predicates in controller queries.
Example direction:
- validate a required identity claim in the JWT bearer events pipeline
- make owner filter logic equivalent to
CurrentUserId != null && x.OwnerUserId == CurrentUserId
High-Risk Candidates Not Fully Confirmed In This Runtime
These are not counted as confirmed findings yet, but they remain serious candidates.
A. Raw-id child endpoints rely on implicit owner scoping
- Category: Authorization issues
- Components:
JobTrackerApi/Controllers/AttachmentsController.csJobTrackerApi/Controllers/CorrespondenceController.cs- selected job-linked endpoints
- Risk: Medium to High
Patterns observed:
- existence checks like
AnyAsync(j => j.Id == jobId) - later child fetches through parent relationships
- reliance on EF global filters instead of explicit per-request owner predicates
If the owner filter is ever bypassed, weakened, or accidentally ignored, these become cross-user read/write primitives.
This runtime could not prove the full cross-user exploit path because the active SQLite file is missing core domain tables (Companies, JobApplications, RuleSettings) and those requests fail before authorization behavior can be fully exercised.
Suggested fix
Add explicit owner predicates in the endpoint queries themselves instead of trusting global filters as the only boundary.
Tested Surfaces With No Confirmed Finding In This Pass
Anonymous API reachability
Observed anonymous results in this runtime:
GET /api/export/jobs→401POST /api/backup/encrypted→401POST /api/jobimport/preview→401POST /api/client-errors→401
This is better than the earlier pre-hardening posture.
Gmail OAuth callback
GET /api/gmail/oauth/callback?code=fake&state=fakereturned a generic failure page- no secret data exposure observed in this pass
File upload path traversal via visible filename
Code review on:
AuthController.UploadAvatarAttachmentsController.UploadAttachmentsController.RenameProfileCvController.Upload
Current handling uses Path.GetFileName(...) and generated storage names, which is a reasonable defense against straightforward path traversal through user-supplied filenames.
No confirmed traversal exploit in this pass.
Recommended Remediation Order
-
Fix authenticated SSRF in job import preview
- hostname resolution checks
- no internal/private destinations
- preferably domain allowlist
-
Fix JWT subjectless-auth acceptance and owner-filter allow-all behavior
- require subject/nameidentifier
- reject tokens that do not map to a real app identity
- change owner filters to deny on null current user
-
Harden raw-id owner-sensitive endpoints with explicit owner predicates
- attachments
- correspondence
- job-linked child endpoints
-
Run a second exploit pass after the fixes
- repeat cross-user probes
- retest SSRF bypasses
- fuzz upload/parser surfaces further
Evidence Summary
Runtime probes performed
- anonymous reachability checks against auth/config, csrf, auth/me, client-errors, jobimport preview, export, backup, and Gmail callback
- authenticated SSRF probes against job import preview using loopback-resolving hostnames
- authenticated malformed-token probe using a signed local JWT without subject/nameidentifier
Key observed outputs
/api/jobimport/previewacceptedhttp://127.0.0.1.nip.io:5202/api/auth/config/api/auth/mereturned200for a signed local JWT without subject/nameidentifier- owner filters in
JobTrackerContextexplicitly allow all rows whenCurrentUserId == null
Honest Boundaries
- Some cross-user raw-id probes were limited by the local runtime using an incomplete SQLite schema.
- Those areas are reported as high-risk candidates, not falsely upgraded to confirmed findings.
- The two confirmed findings above are supported by direct runtime evidence plus code-path verification.