AI speed. Human accountability.
CredShields · Web Application Penetration Test
Web Application & API Pentest
Emory Pro
Emory Pro is a product of Salico GmbH
This report presents the findings of a web application penetration test conducted against Emory Pro Web applications and related APIs. Findings were identified and verified by the CredShields security team.
- Critical1
- High4
- Medium8
- Low18
- Info6
- Client
- Emory Pro
- Scope
- Emory Pro Web applications and related APIs
/ Contents
Table of Contents
- 01Executive Summary
- 02Vulnerability Summary
- 03Detailed Findings
- C001Vertical Privilege Escalation to Organization Admin (Unauthorized Role Assignment / Mass Assignment)Critical
- H001Broken Authorization & Mass Assignment on Team-Membership EndpointsHigh
- H002Broken Function-Level Authorization on Task-Template ManagementHigh
- H003Unauthorized Destructive Modification of Shared Stage ConfigurationHigh
- H004Lower-Privileged Role Can Suspend / Disable Organization Admin AccountsHigh
- M001Insufficient Session Expiration; Refresh Token Usable as Access Token; No RevocationMedium
- M002Unauthenticated Exposure of Inspection Reports & Public S3 PDFs (PII)Medium
- M003Read-Only Customer Can Write Files to the Organization S3 BucketMedium
- M004Internal Staff Directory / User Roster Disclosed to External & Low-Privilege RolesMedium
- M005Broken Object-Property Authorization: Unvalidated / Cross-Org item_type_ids in TemplatesMedium
- M006Backend API Served Over Cleartext HTTP With No HTTPS EnforcementMedium
- M007Mass Assignment of Internal Team State Flags / Creation of Hidden TeamsMedium
- M008Backend API Directly Exposed on Plaintext TCP/3000 (CDN/WAF Bypass)Medium
- L001Username Enumeration via Login Response & Timing OracleLow
- L002No Rate Limiting, Lockout, or CAPTCHA on LoginLow
- L003No Rate Limiting on Password-Reset (Email Flooding)Low
- L004SQL Error / Stack-Trace Information Disclosure (Not Exploitable Injection)Low
- L005Missing Security Headers / No HSTS / Clickjacking on the FrontendLow
- L006JavaScript Source Maps Publicly Exposed (incl. Sibling Hosts)Low
- L007API Documentation (Swagger/OpenAPI) Exposed to Low-Privilege UsersLow
- L008Public Superadmin Login InterfaceLow
- L009Missing Function-Level Authorization on Bulk-Upload Endpoints (Suspected)Low
- L010Public WordPress Admin Login & REST API on Marketing Host emory.proLow
- L011Exposed Hidden Sibling / Logistics HostsLow
- L012Android App Links Delegated to Dev/Staging Mobile PackagesLow
- L013Known-Vulnerable Bundled Dependency: elliptic 6.5.2Low
- L014Third-Party Stylesheets Loaded Without Subresource IntegrityLow
- L015Task-Template Input-Validation Gaps (Negative Sequence, Duplicate IDs)Low
- L016Stored Unsanitized HTML in Task-Template NameLow
- L017Weak Password PolicyLow
- L018Weak Bot Protection on Signup (Invisible CAPTCHA)Low
- I001Misleading Success Response on No-Op / Cross-Tenant Archive WritesInfo
- I002Unused Tenant-ID Path Parameter on Organization Access-Flags EndpointInfo
- I003Disallowed CORS Origins Receive HTTP 500 Instead of a Normal ResponseInfo
- I004Server Technology Disclosure via Response HeadersInfo
- I005Public Development / Staging Environment Reachable on the InternetInfo
- I006Deprecated TLS 1.0 / 1.1 Reported on the Backend (Unconfirmed)Info
- 04Assessment Notes
- 05Reviewers & Signatories
/ Executive Summary
The picture at a glance
A grey-box web application and API penetration test was conducted for Salico GmbH on their Emory Pro platform — the web front-end (dev-fe.emory.pro) and its REST API (dev-be.emory.pro) — across multiple authenticated roles and an unauthenticated perspective. The assessment identified a total of 37 findings, comprising 1 Critical, 4 High, 8 Medium, 18 Low, and 6 Informational severity issues.
The single Critical finding, a Vertical Privilege Escalation to Organization Admin, allowed unauthorized role assignment and mass assignment, posing a severe risk to administrative control. The 4 High findings highlighted further authorization weaknesses — broken authorization and mass assignment on team-membership endpoints, task-template management, unauthorized destructive modification of shared stage configuration, and the ability for lower-privileged roles to suspend Organization Admin accounts. Overall the assessment revealed a prevalence of broken access-control and authorization flaws across the application and its APIs, alongside unauthenticated exposure of inspection reports and public S3 PDFs containing PII, transport weaknesses (cleartext HTTP and a plaintext TCP/3000 CDN/WAF bypass), and a range of enumeration, rate-limiting, information-disclosure, and infrastructure-exposure issues.
Following remediation, a retest confirmed that 35 of the 37 findings have been fixed. The remaining 2 — server technology disclosure via response headers, and the public development/staging environment reachable on the internet — were reviewed and formally accepted as risks by Salico GmbH. Per-finding retest status is recorded inline.
Assessment overview
The engagement involved a comprehensive web application penetration test targeting Salico GmbH's Emory Pro Web applications and related APIs. The primary objective was to identify security vulnerabilities that could compromise the confidentiality, integrity, or availability of sensitive data and system functionality. This assessment included a thorough review of authentication mechanisms, authorization controls, input validation, session management, and overall application logic, utilizing both automated tools and manual testing techniques to simulate real-world attack scenarios across various user roles.
The assessment identified a total of 37 findings, comprising one critical, four high, eight medium, eighteen low, and six informational findings. The most severe issues revolved around significant deficiencies in authorization and access control. A critical vulnerability, "Vertical Privilege Escalation to Organization Admin (Unauthorized Role Assignment / Mass Assignment)," demonstrated a complete bypass of role-based access controls, allowing a low-privileged user to gain full administrative control. Complementing this, several high severity findings highlighted pervasive broken authorization, such as "Broken Authorization & Mass Assignment on Team-Membership Endpoints," "Unauthorized Destructive Modification of Shared Stage Configuration," and the ability for lower-privileged roles to "Suspend / Disable Organization Admin Accounts," indicating a systemic problem with enforcing appropriate access for different user types.
Further significant concerns were categorized as medium severity. These included "Insufficient Session Expiration; Refresh Token Usable as Access Token; No Revocation," which could lead to prolonged unauthorized access, and "Unauthenticated Exposure of Inspection Reports & Public S3 PDFs (PII)," revealing sensitive personal information without any authentication. Other medium findings pointed to inadequate defense-in-depth, such as a "Read-Only Customer Can Write Files to the Organization S3 Bucket," and direct backend exposure through "Backend API Served Over Cleartext HTTP With No HTTPS Enforcement" and "Backend API Directly Exposed on Plaintext TCP/3000," bypassing security controls and transport layer security.
The eighteen low severity findings covered a broad range of issues, from "Username Enumeration via Login Response & Timing Oracle" and a lack of "No Rate Limiting, Lockout, or CAPTCHA on Login," to various information disclosure vulnerabilities like exposed API documentation, JavaScript source maps, and sensitive internal directories. Additionally, several security misconfigurations, such as missing security headers, use of known-vulnerable dependencies, and weak password policies, contributed to an increased attack surface. The six informational findings provided minor observations and recommendations for further hardening.
Overall, the initial assessment revealed significant security weaknesses, particularly in authorization and access control, which could lead to severe business impact if exploited. However, it is commendable that Salico GmbH has taken swift action to address these concerns. A retest indicated a strong remediation effort, with 35 out of the 37 identified findings confirmed as fixed and one acknowledged. This demonstrates a strong commitment to improving the security posture of the Emory Pro Web applications and APIs.
Audited scope
Retested scope
Engagement details
- Client
- Emory Pro
- Assessment type
- Grey-box Assessment
- Method
- Black-box testing
- Engagement
- Emory Pro Web Application & API Security Assessment
- Report date
- July 1, 2026
- Start date
- June 5, 2026
- End date
- June 8, 2026
- Retest date
- July 1, 2026
- Total findings
- 37
Finding severity distribution
/ Vulnerability Summary
Every finding at a glance
Ordered by severity. Follow an ID for the full write-up.
/ Detailed Findings
37 findings across the assessment
Findings are ordered by severity.
C001 · Privilege Escalation / Mass Assignment (Broken Function-Level Authorization) — OWASP API1/API3/API5 · CWE-269, CWE-915, CWE-639
Vertical Privilege Escalation to Organization Admin (Unauthorized Role Assignment / Mass Assignment)
Description
The backend trusts a client-supplied user_role value with no check on whether the caller is authorized to assign that role, so lower-privileged accounts can grant themselves or others the Organization Admin role. Two independent single-request vectors were confirmed: an internal user changing their own role on the self-update endpoint, and a Warehouse Admin creating a brand-new Organization Admin via the member-creation endpoint. Either way the result is full administrative control of the organization, which collapses the entire role-based access-control model.
Affected endpoints
- PUT https://dev-be.emory.pro/user/update/{id}
- POST https://dev-be.emory.pro/user/signup
Evidence
A Warehouse Admin minted a full Organization Admin (uid 209/212) via a single POST /user/signup request, and an External Field Agent self-elevated to role OA.
Proof of Concept
- Vector A: log in as an External Field Agent at https://www.dev-fe.emory.pro/login and note the user_id and role EFA.
- Send PUT /user/update/{own_id} with body user_role 3, receive HTTP 200 success, then re-authenticate and observe the new JWT decodes to role OA.
- Vector B: log in as a Warehouse Admin and send POST /user/signup with user_role 3, receiving HTTP 201 Created.
- As WA, GET /user/organization?search=new... shows the created account with user_role 3 and user_role_key OA.
HTTP Request
POST /user/signup HTTP/2
Host: dev-be.emory.pro
Authorization: Bearer <WAREHOUSE_ADMIN_JWT>
Content-Type: application/json
Origin: https://www.dev-fe.emory.pro
source: web
{'full_name':'WA Escalation Test','email':'wa-oa-esc@example.com','user_role':3,'language_id':1,'employee_id':'WAESC01'}Impact
Complete compromise of an organization tenant by a lower-trust account. The attacker can create persistent backdoor admins and gains full read, modify and delete over all org data and settings. Because the role boundary is the central security control, this undermines every other access control in the platform.
Remediation
- On both PUT /user/update/:id and POST /user/signup, apply a strict server-side allow-list of assignable roles so a caller can never assign a role at or above its own; strip user_role from self-update unless authorized.
- Re-derive authorization from a trusted server-side store on every request rather than from the JWT role claim alone.
- Add audit logging and alerting on every role change and admin creation, plus negative authorization tests proving WA/SFA/IFA/EFA cannot become or create OA.
Retest
Retested: FIXED — EFA PUT /user/update/197 with user_role 3 returns 403 (cannot assign a role higher than your own); WA POST /user/signup with user_role 3 returns 403.
H001 · Broken Function/Object-Level Authorization + Mass Assignment — OWASP API1/API3/API5 · CWE-285, CWE-639, CWE-915
Broken Authorization & Mass Assignment on Team-Membership Endpoints
Description
The team-membership write endpoints perform no object-level or role authorization and accept client-controlled role, user_id_ref and status fields. Lower-privileged roles (confirmed with Senior Field Agent and Warehouse Admin) can add arbitrary users to teams, delete or modify memberships they do not own, re-point a membership at any arbitrary user by writing user_id_ref (an impersonation/BOLA primitive), silently hide active members with status 0, and persist out-of-set role strings such as owner. The frontend restricts these actions but the backend does not.
Affected endpoints
- POST https://dev-be.emory.pro/teams/{teamId}/members
- PUT https://dev-be.emory.pro/teams/{teamId}/members/{memberId}
- DELETE https://dev-be.emory.pro/teams/{teamId}/members/{memberId}
Evidence
A Senior Field Agent re-pointed a membership it did not own to uid 204 via PUT (HTTP 204) and deleted others memberships, and a Warehouse Admin hid a member with status 0.
Proof of Concept
- As OA, create a team and add WA/SFA/OA members and record member IDs.
- As Senior Field Agent, PUT /teams/{T}/members/{member_not_owned} with body user_id_ref 204, receive HTTP 204, and GET shows the membership re-pointed to uid 204.
- As Senior Field Agent, DELETE /teams/{T}/members/{member_not_owned} returns HTTP 204, and POST /teams/{T2}/members with user_id_ref 205 and role member returns HTTP 201 success.
- As Warehouse Admin, PUT /teams/{T}/members/{id} with body status 0 returns HTTP 204 and the member disappears from the roster GET.
Impact
Lower-privileged roles can tamper with team composition, hide members, and impersonate users within the team context. Unexpected role strings such as owner entering the model may be trusted by downstream authorization logic. It confirms a systemic pattern where write endpoints rely on frontend checks only.
Remediation
- Enforce server-side authorization on every /teams/{id}/members write, verifying both the caller permission and that the target membership belongs to a team the caller may administer.
- Ignore or strip client-supplied user_id_ref and status on update, and validate role against an allow-list (member, admin) with a DB CHECK or enum.
- Add negative tests for SFA/WA/IFA/EFA against each membership verb.
Retest
Retested: FIXED — SFA POST/PUT/DELETE on /teams/{id}/members return 403 and a WA user_id_ref rewrite returns 400 (valid user id). Residual: WA can still persist the free-text role owner (client-acknowledged, no downstream effect).
H002 · Broken Function-Level Authorization — OWASP API5 · CWE-285, CWE-862
Broken Function-Level Authorization on Task-Template Management
Description
Organization task-template management is gated by role inconsistently and with no dedicated permission. An Internal Field Agent created an org-wide task template, and a Senior Field Agent modified and deleted templates it did not own (including an OA-owned template), despite neither role holding any template-management capability flag. Task templates drive the inspection workflow org-wide, so unauthorized create, modify and delete is an integrity and availability issue affecting all users of the organization.
Affected endpoints
- POST https://dev-be.emory.pro/tasks/templates
- PUT https://dev-be.emory.pro/tasks/templates/{id}
- DELETE https://dev-be.emory.pro/tasks/templates/{id}
Evidence
An Internal Field Agent created an org-wide template (HTTP 201) and a Senior Field Agent updated and deleted an OA-owned template (HTTP 200).
Proof of Concept
- As Internal Field Agent, POST /tasks/templates with a minimal body returns HTTP 201 Task template created successfully.
- As OA create a victim template V, then as Senior Field Agent PUT /tasks/templates/V returns HTTP 200 updated.
- As Senior Field Agent, DELETE /tasks/templates/V (OA-owned) returns HTTP 200 deleted.
Impact
Field roles could author, alter and destroy the inspection task templates the whole organization depends on. There was no object-ownership check and no dedicated manage-templates permission, exposing org-wide workflow configuration to integrity and availability abuse.
Remediation
- Introduce an explicit manage_templates capability and enforce it server-side on all /tasks/templates verbs.
- Add object-ownership and scope checks on update and delete.
- Replace soft-delete-without-authz with an authorized, audited delete.
Retest
Retested: FIXED — IFA and SFA POST /tasks/templates return 403; only OA and WA can manage templates.
H003 · Broken Function-Level Authorization (Integrity, Destructive) — OWASP API5 · CWE-285, CWE-460
Unauthorized Destructive Modification of Shared Stage Configuration
Description
A Warehouse Admin whose permission set explicitly carries manage_screen_settings false was able to mutate the organization shared stage dropdown configuration via POST /dropdown/dropdown-options. The request returns HTTP 400 (a JSON validation error) but the destructive part executes anyway: the existing active stage (named New) was deleted despite the error response. An unauthorized role can therefore wipe shared configuration that every user of the org relies on, and the API masks it as a failed request.
Affected endpoints
- POST https://dev-be.emory.pro/dropdown/dropdown-options
Evidence
After a WA POST returned HTTP 400, GET /dropdown?type=stage returned an empty array, confirming the active stage was destroyed on a failed request.
Proof of Concept
- As Warehouse Admin confirm GET /user/logged-user-permissions shows manage_screen_settings false.
- GET /dropdown?type=stage returns one active stage named New.
- POST /dropdown/dropdown-options with an options body returns HTTP 400 validation error.
- GET /dropdown?type=stage again returns an empty array, showing the existing stage was deleted despite the 400.
Impact
A non-settings role could silently destroy shared stage configuration org-wide, an availability and integrity impact for all users. The 400 error response hid the impact from operators and from naive testing, making the destructive side effect easy to miss.
Remediation
- Enforce the manage_screen_settings capability server-side on /dropdown/dropdown-options and all stage/config management endpoints.
- Wrap mutations in a transaction so a validation failure rolls back, and never partially apply destructive operations before returning a 4xx.
Retest
Retested: FIXED — a WA invalid POST /dropdown/dropdown-options returns 400 and the existing stage is preserved before and after (transactional rollback, no data loss).
H004 · Broken Function-Level Authorization (User Lifecycle / Availability) — OWASP API5 · CWE-285, CWE-269
Lower-Privileged Role Can Suspend / Disable Organization Admin Accounts
Description
A Warehouse Admin can disable arbitrary accounts including Organization Admins via PUT /user/suspend/{id}, which returns 200 success and sets the target status to 0. There is no corresponding activate endpoint (it returns 404), so a suspended Org Admin cannot be cleanly re-enabled through the obvious path. Combined with F1 (a WA can also create Org Admins), a lower-priv role can both manufacture admins and disable the legitimate ones, a direct attack on separation of duties and on availability of the highest-privilege role.
Affected endpoints
- PUT https://dev-be.emory.pro/user/suspend/{id}
Evidence
A Warehouse Admin suspended an Organization Admin via PUT /user/suspend/{oa_id} returning HTTP 200 success and the target showed status 0 with no activate endpoint to reverse it.
Proof of Concept
- As Warehouse Admin identify an Organization Admin user_id or create a throwaway OA via F1 vector B.
- PUT /user/suspend/{oa_id} with an empty body returns HTTP 200 success.
- As OA, GET /user/details/{oa_id} shows status 0 while user_role_key remains OA, and no activate endpoint exists (404) to reverse it.
Impact
A lower-privileged role could lock out organization administrators, a denial of administration. The absence of any self-service re-activation path increased the operational impact and, chained with F1, enabled a full separation-of-duties bypass.
Remediation
- Restrict PUT /user/suspend/:id to callers with explicit member-management authority and forbid suspending users at or above the caller role.
- Provide an authorized, audited re-activation endpoint and log all suspend and activate actions.
Retest
Retested: FIXED — WA PUT /user/suspend/{oa_id} returns 403 (cannot suspend a user who outranks you).
M001 · Insecure Session Lifetime / Broken Authentication — OWASP API2 · CWE-613, CWE-287, CWE-522
Insufficient Session Expiration; Refresh Token Usable as Access Token; No Revocation
Description
Sessions are stateless HS256 JWTs with no server-side revocation. The access token lives about 30 days and the refresh token about 50 days. The access and refresh JWTs share an identical payload differing only by expiry, with no token-type claim, so the long-lived refresh token is directly accepted as a bearer access token on protected endpoints. Logout is client-side only and changing the password does not invalidate prior tokens. A token captured once is usable for weeks and cannot be revoked.
Affected endpoints
- POST https://dev-be.emory.pro/user/authenticate
- GET https://dev-be.emory.pro/user/logged-user-permissions
- (applies to all authenticated dev-be.emory.pro endpoints for token validation)
Evidence
The long-lived refresh token was accepted directly as a bearer on GET /user/logged-user-permissions (HTTP 200), and a pre-password-change access token still worked after the password change.
Proof of Concept
- Log in and decode accessToken and refreshToken from localStorage, noting the about 30-day and 50-day expiries and otherwise identical claims with no type field.
- Use the refresh token as an Authorization bearer on GET /user/organization, receiving HTTP 200.
- Change the password, then reuse the pre-change access token on a protected endpoint and observe it still returns 200.
Impact
Stolen sessions could not be contained: password reset and logout did not stop an active attacker, and a single token leak yielded up to 50 days of unrevocable access. The refresh token doubling as an access credential broadened the exposure, and cleartext transport findings amplified the capture risk.
Remediation
- Add a distinct token-type claim and a separate refresh signing key or audience, and reject refresh tokens at access-protected endpoints.
- Shorten access-token lifetime to minutes and implement rotating refresh tokens.
- Add server-side revocation via a per-user token_version or password_changed_at claim checked each request, invalidated on password change and real server-side logout.
Retest
Retested: FIXED (core) — a refresh token used as an access bearer now returns 401 and the JWT carries token_type and token_version. Residual: access and refresh lifetimes remain about 30/50 days and revocation-on-password-change was not independently verified (no inbox), but the token_version mechanism is now present.
M002 · Sensitive Information Disclosure (Broken Authentication on Resource / Public Object Storage) · CWE-200, CWE-552
Unauthenticated Exposure of Inspection Reports & Public S3 PDFs (PII)
Description
Inspection check reports are retrievable with no authentication: GET /check-report/{identifier} returns the full report (item details, assignee name and employee ID, images, all inspection fields), and the corresponding PDF was stored in a publicly readable S3 location with no auth or expiry. The identifier is high-entropy (about 64-bit random), so this is a share-link or public-object exposure rather than an enumerable IDOR. Anyone who obtains a report URL can read customer and operational PII indefinitely.
Affected endpoints
- GET https://dev-be.emory.pro/check-report/{identifier}
- GET https://emory-files.s3.us-west-2.amazonaws.com/<org_uuid>/checkreport/<...>.pdf
- GET https://www.dev-fe.emory.pro/cr/{identifier}
Evidence
With no Authorization header, GET /check-report/{identifier} returned HTTP 200 full report JSON and the S3 PDF returned HTTP 200 application/pdf with no credentials.
Proof of Concept
- As OA generate an inspection report via POST /item/generate-check-report/{id}, which returns a pdf URL and a public /cr/{identifier} link.
- In a session with no Authorization header, GET https://dev-be.emory.pro/check-report/{identifier} returns HTTP 200 with full report JSON.
- GET the S3 PDF directly with no credentials returns HTTP 200 application/pdf.
Impact
Customer and operational PII was exposed to anyone holding a report link, and durable public PDFs survived in-app revocation, creating potential GDPR exposure. The exposure was durable because the objects had no expiry.
Remediation
- Serve report PDFs via short-lived presigned URLs and remove public-read from the checkreport prefix.
- Add optional access control to share links (login or per-report passcode or expiry) for PII reports.
- Set Referrer-Policy no-referrer and X-Robots-Tag noindex on /cr/ and log and rate-limit report fetches.
Retest
Retested: FIXED — check-report/{id} now returns pdf_link as a 60-minute presigned URL and the bare S3 path returns 403. Per client design the report is intentionally a high-entropy non-enumerable capability link, so unauthenticated access to /check-report/{id} is by design and accepted as fixed.
M003 · Broken Access Control (Read-Only Enforcement Bypass) — OWASP API5 · CWE-285, CWE-732
Read-Only Customer Can Write Files to the Organization S3 Bucket
Description
A Customer account flagged strictly read-only (is_customer_readonly true) could still obtain a valid presigned S3 PUT URL from POST /file/get-signed-url and upload arbitrary content into the org bucket. The object key is correctly scoped to the caller own org prefix (no cross-tenant write, no traversal), but this violates the read-only contract and is a storage-abuse and content-injection vector. The customer permission set is itself inconsistent, reporting manage flags true despite is_customer_readonly true.
Affected endpoints
- POST https://dev-be.emory.pro/file/get-signed-url
Evidence
A read-only Customer obtained a presigned upload_url and PUT arbitrary content, writing an object to the org bucket.
Proof of Concept
- Log in as a read-only Customer and POST /file/get-signed-url with a file_name and content_type, receiving HTTP 200 with a presigned upload_url.
- PUT arbitrary content to that URL, receiving HTTP 200 with the object written to the org bucket.
Impact
A read-only account could inject or abuse storage in the org bucket, breaching the read-only contract. Impact was bounded to the caller own org prefix with no cross-tenant write, so the exposure was storage abuse and content injection rather than data theft.
Remediation
- Gate /file/get-signed-url and all upload endpoints behind the same write-permission middleware as /item and /container, and deny when is_customer_readonly.
- Make is_customer_readonly true an overriding deny, never co-grant manage flags, and use short presigned-URL TTLs.
Retest
Retested: FIXED — read-only Customer 98 POST /file/get-signed-url returns 403 (no presign issued). Residual: the permission set still inconsistently reports manage_item, manage_container and assign_objects true.
M004 · Excessive Data Exposure (Broken Function-Level Authorization) — OWASP API3 · CWE-200, CWE-285
Internal Staff Directory / User Roster Disclosed to External & Low-Privilege Roles
Description
GET /user/organization returns the full internal roster with each member full name, email, employee ID and role, and is accessible to roles that should not enumerate staff: External Field Agents, read-only Customers, and Internal Field Agents (IFA holds manage_member false yet read all 10 org users including the Org Admin account and email). This leaks internal personnel PII and a ready-made phishing target list to the least-trusted accounts.
Affected endpoints
- GET https://dev-be.emory.pro/user/organization
Evidence
A read-only Customer, External Field Agent and Internal Field Agent each retrieved all org members with full_name, email, employee_id and role including the Org Admin (HTTP 200).
Proof of Concept
- Log in as a read-only Customer, External Field Agent, or Internal Field Agent.
- GET /user/organization returns HTTP 200 with all members including full_name, email, employee_id and user_role_key, including the Org Admin.
Impact
Internal staff names, emails, employee IDs and roles were exposed to external and low-privilege accounts, providing a phishing and social-engineering target list. Exposure was bounded to one organization but included the Org Admin account.
Remediation
- Restrict GET /user/organization to roles with viewMembers or manage_member and deny external, customer and field roles.
- For roles that need a limited assignee picker, return only display name and id and never email or employee_id.
Retest
Retested: FIXED — GET /user/organization returns empty for EFA and Customer, and IFA sees names only with no email or employee_id.
M005 · Broken Object Property Level Authorization / Improper Input Validation — OWASP API3/API6 · CWE-639, CWE-20
Broken Object-Property Authorization: Unvalidated / Cross-Org item_type_ids in Templates
Description
POST /tasks/templates accepts arbitrary item_type_ids with no referential or ownership validation. A low-privilege Internal Field Agent created templates referencing an item_type_id belonging to another organization (id 1 resolved to a foreign org Hatchback) and a nonexistent id (999999, persisted with item_type_name null). This is a cross-tenant object-reference leak and an integrity gap reachable by a field role.
Affected endpoints
- POST https://dev-be.emory.pro/tasks/templates
Evidence
An Internal Field Agent created templates that resolved a foreign org item-type name (id 1) and persisted a nonexistent id 999999 with item_type_name null (HTTP 201).
Proof of Concept
- As Internal Field Agent, POST /tasks/templates with item_type_ids [1] returns 201 and GET shows a foreign org item-type name resolved into the template.
- Repeat with item_type_ids [999999], receiving 201 with the reference persisted as item_type_name null.
Impact
A low-privilege role could pull a cross-org object reference into a template and persist invalid references, a cross-tenant reference leak and data-integrity gap. No bulk exfiltration was demonstrated, bounding the impact.
Remediation
- Validate every referenced item_type_id exists and belongs to the caller organization, rejecting otherwise with 400 or 403.
- Apply the same scoping to all endpoints that accept object-id arrays.
Retest
Retested: FIXED — the low-priv path is closed (via F3) and an OA request with item_type_ids [1,999999] returns 400 Invalid item_type_ids, confirming referential validation was added.
M006 · Cleartext Transmission of Sensitive Information (Missing Transport Security) · CWE-319, CWE-523
Backend API Served Over Cleartext HTTP With No HTTPS Enforcement
Description
dev-be.emory.pro accepts and serves the live API over plaintext HTTP and does not redirect to HTTPS. http://dev-be.emory.pro/health returns 200 over HTTP, and authenticated requests (for example openapi.json with a bearer token) succeed in cleartext. A network or MITM attacker (rogue Wi-Fi, on-path, SSL-stripping) can read or modify tokens and data, greatly amplified by F6 (30 to 50-day non-revocable tokens), so one interception yields weeks of access.
Affected endpoints
- GET http://dev-be.emory.pro/ (and all routes, no HTTP->HTTPS redirect)
- GET http://dev-be.emory.pro/openapi.json
Evidence
http://dev-be.emory.pro/health returned HTTP 200 with no redirect and authenticated http openapi.json returned 200 schema over cleartext.
Proof of Concept
- Run curl -i http://dev-be.emory.pro/health and observe HTTP 200 with no redirect to HTTPS.
- Run curl http://dev-be.emory.pro/openapi.json with an Authorization bearer and observe HTTP 200 schema over cleartext.
Impact
A network attacker could intercept long-lived bearer tokens in cleartext (leading to account takeover) and tamper with requests. The absence of a redirect or HSTS made SSL-stripping straightforward, and chained with F6 a single interception granted weeks of access.
Remediation
- Redirect all port-80 traffic to HTTPS (301) and reject authenticated requests over plaintext.
- Emit Strict-Transport-Security once HTTPS coverage is verified and enforce HTTPS end-to-end from edge to origin.
Retest
Retested: FIXED — http://dev-be.emory.pro/ returns 301 to https and cleartext openapi.json returns 301 with no data served.
M007 · Mass Assignment / Improper Input Validation — OWASP API3/API6 · CWE-915, CWE-642
Mass Assignment of Internal Team State Flags / Creation of Hidden Teams
Description
POST /teams accepts client-supplied internal state flags status and is_active. A Warehouse Admin created a team with status 0 and is_active 0 (HTTP 201) and the stored record carried those flags, so the team is excluded from GET /teams (which filters status 1) yet remains directly addressable (GET /teams/{id} and GET /teams/{id}/members return 200). This lets a user create hidden-but-reachable teams and mass-assign internal fields the client should not control.
Affected endpoints
- POST https://dev-be.emory.pro/teams
Evidence
A Warehouse Admin created a team with status 0 and is_active 0; it was omitted from GET /teams but GET /teams/{newid} and its members endpoint returned 200.
Proof of Concept
- As Warehouse Admin, POST /teams with status 0 and is_active 0 returns HTTP 201.
- GET /teams omits it, but GET /teams/{newid} and GET /teams/{newid}/members return HTTP 200.
Impact
A user could create hidden-but-addressable objects and mass-assign internal state fields the client should not control, an integrity and data-consistency gap. The hidden objects remained ID-enumerable, so the concealment was weak.
Remediation
- Strip status and is_active and any internal flags from create and update bodies and set them server-side.
- Validate object visibility consistently across list and direct-fetch paths.
Retest
Retested: FIXED — is_active is forced to 1 and status 0 teams now appear in GET /teams (no longer hidden-but-addressable). Residual: status is still client-settable with no security impact.
M008 · Security Misconfiguration (Exposure of Backend Origin / Alternate Port) · CWE-668, CWE-319
Backend API Directly Exposed on Plaintext TCP/3000 (CDN/WAF Bypass)
Description
The backend answers directly on plaintext TCP/3000: http://dev-be.emory.pro:3000/ returns 200 with body success true message Emory Backend APIs v1. This alternate unencrypted port sits outside the standard 443 front door and is not among Cloudflare proxied ports, so it likely bypasses the CDN and WAF and edge protections (for example rate-limiting) while also serving the API in cleartext.
Affected endpoints
- GET http://dev-be.emory.pro:3000/ (and the API surface on this listener)
Evidence
curl to http://dev-be.emory.pro:3000/ returned HTTP 200 with body indicating Emory Backend APIs v1 on a plaintext port.
Proof of Concept
- Run curl http://dev-be.emory.pro:3000/ and observe HTTP 200 with body success true message Emory Backend APIs v1.
Impact
The exposed plaintext port likely bypassed CDN and WAF edge controls including rate limiting, and served the API in cleartext, compounding the transport findings. It gave an attacker a direct origin path outside the protected 443 front door.
Remediation
- Do not publicly expose port 3000; bind to localhost or a private interface and route all traffic via the reverse proxy or CDN on 443 only.
- Restrict the port at the security-group or firewall level to the reverse proxy and enforce HTTPS-only on any app listener.
Retest
Retested: FIXED — http://dev-be.emory.pro:3000/ has no listener (connection refused).
L001 · User Enumeration (Observable Discrepancy) — OWASP API2 · CWE-204, CWE-208
Username Enumeration via Login Response & Timing Oracle
Description
POST /user/authenticate returns an identical status and human message for valid vs invalid emails, but two side channels distinguish registered accounts: the JSON errors.name differs (AuthorizationError for an existing email with wrong password vs Error for a non-existent email), and an about 0.5 second timing delta (bcrypt runs only for existing accounts). Rated Low because the user-facing message is identical and impact is enabling rather than direct, but it meaningfully amplifies F15 and F16.
Affected endpoints
- POST https://dev-be.emory.pro/user/authenticate
Evidence
A known email with wrong password returned errors.name AuthorizationError at about 1.9s, while a random email returned errors.name Error at about 1.4s.
Proof of Concept
- POST /user/authenticate with a known email and wrong password returns 401 with errors.name AuthorizationError and is slower (about 1.9s).
- Repeat with a random email and observe 401 with errors.name Error and faster timing (about 1.4s).
Impact
An unauthenticated attacker could reliably confirm which email addresses are registered, building a target list. The impact was enabling rather than direct, but it amplified the credential-stuffing risk from the missing login rate limiting.
Remediation
- Return byte-identical responses (status, body, errors.name) for all auth failures.
- Always run a constant-time dummy bcrypt comparison even when the email does not exist.
Retest
Retested: FIXED — existing vs non-existent email now return an identical 401 AuthorizationError with the same message and overlapping timing (no oracle).
L002 · Improper Restriction of Excessive Authentication Attempts — OWASP API4 · CWE-307
No Rate Limiting, Lockout, or CAPTCHA on Login
Description
POST /user/authenticate enforces no rate limiting, lockout or CAPTCHA; many consecutive failures all return 401 with no 429. Combined with F14, this enables practical credential stuffing and spraying against enumerated accounts. Signup is CAPTCHA-protected but login is not.
Affected endpoints
- POST https://dev-be.emory.pro/user/authenticate
Evidence
Replaying POST /user/authenticate 12 to 30 times with wrong passwords returned 401 every time with no 429 and no lockout, and the correct password then succeeded immediately.
Proof of Concept
- Replay POST /user/authenticate about 12 to 30 times with wrong passwords for a known account and observe every response 401 with no 429 and no lockout, then the correct password succeeds immediately.
Impact
An unauthenticated attacker could run large-scale credential stuffing and spraying against enumerated accounts, enabling account takeover of weak or reused passwords. Chained with the enumeration oracle, the attack was efficient and targeted.
Remediation
- Add per-account and per-IP rate limiting with exponential backoff, temporary lockout or step-up CAPTCHA after a few failures, and alert on bursts.
Retest
Retested: FIXED — POST /user/authenticate returns 429 after about 5 attempts (per-IP throttle).
L003 · Improper Control of Interaction Frequency (Resource Abuse) — OWASP API4 · CWE-770
No Rate Limiting on Password-Reset (Email Flooding)
Description
POST /user/forgot-password applies no rate limiting; repeated requests for the same address all return 200. An attacker can flood any user inbox with reset emails (harassment or mail-bombing) and drive email cost. The endpoint does not leak account existence, since responses are uniform.
Affected endpoints
- POST https://dev-be.emory.pro/user/forgot-password
Evidence
Sending POST /user/forgot-password 6 to 20 times for the same address returned 200 every time with no 429.
Proof of Concept
- Send POST /user/forgot-password for a victim email 6 to 20 times and observe all 200 with no 429.
Impact
An unauthenticated attacker could mail-bomb any user inbox with reset emails, a harassment and cost-abuse vector. There was no data compromise, keeping the severity low.
Remediation
- Rate-limit per-account and per-IP (for example one email per few minutes) with a daily cap, and add optional CAPTCHA after the first request in a window.
Retest
Retested: FIXED — POST /user/forgot-password returns 429 (Too many password reset requests) after the first request.
L004 · Information Disclosure (Improper Error Handling) — OWASP API8 · CWE-209, CWE-754
SQL Error / Stack-Trace Information Disclosure (Not Exploitable Injection)
Description
Several endpoints leak raw MySQL syntax errors and Node stack traces with server file paths such as /app/lib/controllers/inspection_types.js. These are not exploitable SQL injection: inputs are integer-coerced (a quote becomes NaN, giving Unknown column NaN) or escaped, and even benign numeric input triggers the same error on the malformed query-builder paths. The scanner CWE-89 SQL injection titles are overstated; the real reproducible issue is verbose error and stack-trace disclosure.
Affected endpoints
- GET https://dev-be.emory.pro/item/organization/v2?limit=50
- GET https://dev-be.emory.pro/inspection-types/{id}
- GET https://dev-be.emory.pro/teams/{id}/members
- GET https://dev-be.emory.pro/dropdown?type=stage&type={bad}
Evidence
GET /inspection-types/17abc returned a 400 with a MySQL error and a Node stack trace including /app/lib/ file paths.
Proof of Concept
- GET /item/organization/v2?limit=50 (omit skip) returns 400 with a raw MySQL syntax error.
- GET /inspection-types/17abc returns 400 with a MySQL error and a Node stack trace including /app/lib/ file paths.
Impact
Verbose errors revealed the DB engine, query-builder defects and server file paths to authenticated users, aiding reconnaissance. No data could be read, inferred or altered via injection, keeping the impact informational.
Remediation
- Parameterize all pagination, LIMIT and OFFSET, default skip to 0, and validate or coerce path ids before query construction.
- Add a global error handler returning generic 4xx and 5xx responses and logging detail server-side only.
Retest
Retested: FIXED — GET /item/organization/v2?limit=50 (no skip) returns a generic 400 Something went wrong and the raw MySQL error is gone.
L005 · Security Misconfiguration (Missing Hardening Headers) · CWE-1021, CWE-319
Missing Security Headers / No HSTS / Clickjacking on the Frontend
Description
The SPA origin www.dev-fe.emory.pro (S3 and CloudFront) returns no security headers: no Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options or Referrer-Policy. The app is framable (clickjacking) and lacks transport and script hardening and HSTS (downgrade exposure). The same HSTS gap applies to the sibling host www.webapp.emory.pro. The API origin does send strong Helmet headers; this concerns the frontend origins.
Affected endpoints
- GET https://www.dev-fe.emory.pro/ (all frontend responses)
- GET https://www.webapp.emory.pro/ (all frontend responses)
Evidence
GET https://www.dev-fe.emory.pro/ returned only server AmazonS3 and via cloudfront with none of the standard security headers, and /dashboard rendered inside an attacker frame.
Proof of Concept
- GET https://www.dev-fe.emory.pro/ and observe the response lacks content-security-policy, strict-transport-security, x-frame-options, x-content-type-options and referrer-policy.
- Frame https://www.dev-fe.emory.pro/dashboard from an attacker page and observe it renders with no frame-busting.
Impact
The frontend was framable (clickjacking of a logged-in user) and lacked transport and script hardening, with an HSTS gap enabling protocol downgrade. Impact was UI-redress and downgrade exposure with no direct data compromise.
Remediation
- Add a CloudFront Response Headers Policy delivering Content-Security-Policy (with frame-ancestors self), Strict-Transport-Security, X-Frame-Options DENY, X-Content-Type-Options nosniff and Referrer-Policy no-referrer on all frontend origins.
Retest
Retested: FIXED — dev-fe now serves HSTS, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy and a CSP with frame-ancestors none. Residual: the CSP is Report-Only and Server AmazonS3 is still disclosed (tracked as F35).
L006 · Information Disclosure (Source Code Exposure) · CWE-540, CWE-200
JavaScript Source Maps Publicly Exposed (incl. Sibling Hosts)
Description
The production SPA ships publicly downloadable source maps: main.9553f9da.js.map (about 22 MB) exposes sourcesContent for about 1,841 original TypeScript files, including permission configuration, API routes, hidden subdomain references and S3 bucket names. The sibling host www.webapp.emory.pro exposes its own source map. This hands an attacker the full client source, accelerating discovery of endpoints and role logic such as F1.
Affected endpoints
- GET https://www.dev-fe.emory.pro/static/js/main.9553f9da.js.map
- GET https://www.webapp.emory.pro/static/js/main.e4b05f54.js.map
Evidence
GET https://www.dev-fe.emory.pro/static/js/main.9553f9da.js.map returned HTTP 200 (about 22 MB) with full TS source in sourcesContent, and the webapp sibling map also returned 200.
Proof of Concept
- GET https://www.dev-fe.emory.pro/static/js/main.9553f9da.js.map returns HTTP 200 (about 22 MB) with full TS source in sourcesContent.
- GET https://www.webapp.emory.pro/static/js/main.e4b05f54.js.map returns HTTP 200.
Impact
Public source maps gave an attacker the full client TypeScript source including permission configuration, API routes and S3 bucket names, sharply accelerating reconnaissance and discovery of endpoints and role logic. Alone it enabled recon rather than direct compromise.
Remediation
- Do not deploy .map files to production or restrict them to internal or authenticated access; remove sourceMappingURL from shipped bundles and upload maps privately to your error-monitoring backend.
Retest
Retested: FIXED (dev; prod pending) — dev-fe /assets/*.js.map returns 404 with no sourceMappingURL, but production www.webapp.emory.pro still served a 19 MB map at retest; re-verify after the dev build promotes to prod.
L007 · Security Misconfiguration (Excessive Information Exposure) · CWE-200, CWE-668
API Documentation (Swagger/OpenAPI) Exposed to Low-Privilege Users
Description
The interactive API docs at GET /docs (Scalar/OpenAPI, about 117 paths) and the raw GET /openapi.json load for any authenticated user including a read-only Customer. This discloses the full API surface, including endpoints not used by the customer UI, to the least-trusted accounts, accelerating attack-surface mapping (for example F1, F8, F9).
Affected endpoints
- GET https://dev-be.emory.pro/docs
- GET https://dev-be.emory.pro/openapi.json
Evidence
As a read-only Customer, https://dev-be.emory.pro/docs rendered the full OpenAPI and /openapi.json returned the full schema.
Proof of Concept
- As a read-only Customer open https://dev-be.emory.pro/docs and observe the full OpenAPI renders, and GET /openapi.json returns the full schema.
Impact
The full API surface, including endpoints not used by the customer UI, was disclosed to the least-trusted accounts, accelerating attack-surface mapping for other findings. Impact was reconnaissance value rather than direct compromise.
Remediation
- Restrict /docs and /openapi.json to admin roles or internal networks and disable them in public environments.
Retest
Retested: FIXED — EFA GET /docs and /openapi.json return 403 and unauthenticated /docs returns 401.
L008 · Information Disclosure / Attack-Surface Exposure · CWE-200
Public Superadmin Login Interface
Description
The frontend exposes a routable /superadmin page rendering an admin login form, and the public source maps (F19) contain superadmin-specific source files. This advertises the privileged administration surface to anyone and widens the attack surface for credential attacks against the highest-privilege console.
Affected endpoints
- GET https://www.dev-fe.emory.pro/superadmin
Evidence
GET https://www.dev-fe.emory.pro/superadmin previously rendered an admin login form (HTTP 200).
Proof of Concept
- GET https://www.dev-fe.emory.pro/superadmin and observe it renders an admin login form (HTTP 200).
Impact
The privileged superadmin login and its source were advertised on the public origin, widening the attack surface for credential attacks against the highest-privilege console. It was surface exposure rather than a direct compromise.
Remediation
- Host the admin or superadmin UI on a separate access-controlled origin (VPN, allow-list or SSO) and do not ship its routes or source in the public bundle.
Retest
Retested: FIXED — GET /superadmin returns the SPA shell but the client router renders an in-app 404 Page Not Found with no admin login form, and JS source maps now 404 so the superadmin source files are no longer exposed; residual superadmin string constants route to nothing reachable.
L009 · Broken Function-Level Authorization (Suspected) — OWASP API5 · CWE-862, CWE-285
Missing Function-Level Authorization on Bulk-Upload Endpoints (Suspected)
Description
The bulk-import endpoints do not return 403 for a read-only Customer: the customer token reaches the same import and validation pipeline as an Org Admin, with byte-identical error responses at every malformed input, whereas single-record POST /user/signup and POST /item correctly return 403. This strongly suggests the bulk endpoints lack the function-level gate applied elsewhere; because the user import accepts a User Role column, a successful low-priv bulk import could also be a privilege-escalation path. Actual record creation was not reproduced because the proprietary spreadsheet schema rejected test files.
Affected endpoints
- POST https://dev-be.emory.pro/bulk-upload/user
- POST https://dev-be.emory.pro/bulk-upload/container
- POST https://dev-be.emory.pro/bulk-upload/container_item
Evidence
A read-only Customer POST /bulk-upload/user produced responses byte-identical to an Org Admin at every stage and never 403, while POST /user/signup returned 403.
Proof of Concept
- As a read-only Customer, POST /bulk-upload/user (multipart, field file) with any payload and observe responses identical to an Org Admin at every stage, never 403, whereas POST /user/signup returns 403.
Impact
If confirmed, a low-privilege account could bulk-create records including Org Admin users, a privilege-escalation path via the User Role import column. The barrier observed was the spreadsheet schema, not an authorization control, so the risk depended on where the authz check sits relative to file parsing.
Remediation
- Apply the same role and permission middleware used by /item, /container and /user/signup to all /bulk-upload/* endpoints, with the authz check before any file parsing.
- Validate the User Role column against the caller role-assignment authority and deny for read-only and customer roles.
Retest
Retested: FIXED — EFA POST /bulk-upload/user returns 403 (previously reached the admin pipeline).
L010 · Attack-Surface Exposure / Information Disclosure · CWE-200
Public WordPress Admin Login & REST API on Marketing Host emory.pro
Description
The marketing host emory.pro (and www.emory.pro) runs WordPress with a publicly reachable admin login (/wp-login.php returns 200) and an open REST API index (/wp-json/ returns 200) that enumerates routes and plugin inventory. While distinct from the inspection app, this expands the organization attack surface (WordPress brute-force, plugin-CVE exposure, user enumeration) and was surfaced during scope discovery.
Affected endpoints
- GET https://emory.pro/wp-login.php
- GET https://emory.pro/wp-json/
Evidence
GET https://emory.pro/wp-login.php returned 200 (WordPress login) and GET https://emory.pro/wp-json/ returned a 200 route and plugin index.
Proof of Concept
- GET https://emory.pro/wp-login.php returns 200 (WordPress login) and GET https://emory.pro/wp-json/ returns a 200 route and plugin index.
Impact
The public WordPress admin and REST API expanded the organization attack surface with brute-force, plugin-CVE and user-enumeration exposure. It was distinct from the inspection app and low impact but widened reconnaissance opportunities.
Remediation
- Restrict /wp-login.php and /wp-admin by IP, SSO or 2FA, disable or restrict the WordPress REST API user and route enumeration, and keep WP core and plugins patched.
Retest
Retested: FIXED — the site migrated to Framer; /wp-login.php returns 404 and /wp-json/wp/v2/users returns non-WP HTML with no REST or user enumeration.
L011 · Attack-Surface Exposure (Unlisted Hosts) · CWE-200, CWE-668
Exposed Hidden Sibling / Logistics Hosts
Description
Unlisted sibling hosts respond publicly, broadening attack surface beyond the documented scope: werkerapp-heinrichs.emory.pro (a logistics management interface) returns 200, and www.webapp.emory.pro serves a production frontend with its own exposed source maps (see F19). These should be inventoried, access-controlled or decommissioned.
Affected endpoints
- GET https://werkerapp-heinrichs.emory.pro/
- GET https://www.webapp.emory.pro/
Evidence
GET https://werkerapp-heinrichs.emory.pro/ and GET https://www.webapp.emory.pro/ both returned HTTP 200.
Proof of Concept
- GET https://werkerapp-heinrichs.emory.pro/ returns 200 and GET https://www.webapp.emory.pro/ returns 200.
Impact
Additional public management UIs and a production frontend with exposed source maps broadened the organization attack surface beyond the documented scope. The exposure required host discovery, bounding likelihood.
Remediation
- Inventory all emory.pro hosts, place non-public interfaces behind VPN, SSO or IP allow-listing, and decommission unused environments and remove their source maps.
Retest
Retested: FIXED (dev; prod pending) — werkerapp was client-descoped as a separate demo project, but www.webapp.emory.pro (prod) was still reachable and serving source maps at retest; resolved once the dev changes ship to prod, re-verify then.
L012 · Security Misconfiguration (Mobile Deep-Link Delegation) · CWE-16
Android App Links Delegated to Dev/Staging Mobile Packages
Description
The production web origin /.well-known/assetlinks.json delegates Android App Links (delegate_permission/common.handle_all_urls) to mobile packages such as pro.emory.inspections with specific signing fingerprints. If any delegated package corresponds to a development or staging or otherwise less-controlled build, that app can claim and intercept the production domain deep links.
Affected endpoints
- GET https://www.dev-fe.emory.pro/.well-known/assetlinks.json
Evidence
GET https://dev-fe.emory.pro/.well-known/assetlinks.json returned HTTP/2 200 still delegating to pro.emory.inspections.dev with fingerprint AD:A9:9B:CB:D2:3E:99:2B:...:CC:2F.
Proof of Concept
- GET https://dev-fe.emory.pro/.well-known/assetlinks.json and observe the delegated android_app package(s) and SHA-256 fingerprints, including the dev/staging package pro.emory.inspections.dev.
Impact
A dev or staging (less-controlled) Android package delegated in production assetlinks could claim and intercept the production domain deep links, enabling deep-link interception and phishing on mobile. Exploitation required control of or trust in a delegated package.
Remediation
- Delegate App Links only to production-signed packages and remove dev and staging package fingerprints from the production assetlinks.json.
Retest
Retested: FIXED (code fix verified; client-confirmed, pending production deployment). The client added a build-time generator (genWellKnown.mjs) that produces per-environment App Links / Universal Links association files, so each web origin delegates deep links only to its own signed build — prod (webapp.emory.pro) delegates only to pro.emory.inspections, dev (dev-fe.emory.pro) only to pro.emory.inspections.dev, with unknown/unset env failing safe to prod. This prevents the production origin from vouching for a dev/staging signing key. Client note: we have this file in place; it ensures cross-environment leaking does not take place; as these changes are not in prod yet you would not be able to check that; dev-fe.emory.pro points to the dev app and webapp.emory.pro to the prod app. CredShields verified dev-fe.emory.pro now delegates only to the dev package; re-verify the production file once the change is promoted to webapp.emory.pro.
L013 · Use of Components with Known Vulnerabilities — OWASP A06 · CWE-1104
Known-Vulnerable Bundled Dependency: elliptic 6.5.2
Description
The production JavaScript bundle fingerprints elliptic 6.5.2, a version with known cryptographic-signature-handling advisories (later patched in the 6.5.x line). Risk depends on whether the affected ECDSA code paths are exercised client-side, but the outdated component should be upgraded.
Affected endpoints
- GET https://www.dev-fe.emory.pro/static/js/main.9553f9da.js
Evidence
elliptic version 6.5.2 was fingerprinted in the production bundle and source map.
Proof of Concept
- Identify elliptic 6.5.2 in the bundle or source map.
Impact
A known-vulnerable crypto dependency was shipped to the client, introducing a cryptographic signature-handling weakness if the affected ECDSA code paths were exercised. Impact was contingent on the reachable code paths.
Remediation
- Upgrade elliptic to the latest patched release and run npm audit or SCA in CI, tracking transitive crypto dependencies.
Retest
Retested: FIXED — elliptic is absent from all dev-fe Vite chunks.
L014 · Security Misconfiguration (Missing SRI) · CWE-829
Third-Party Stylesheets Loaded Without Subresource Integrity
Description
The frontend loads third-party-hosted stylesheets without a Subresource Integrity (integrity) attribute, so a compromise of or MITM against the third-party origin could inject attacker-controlled CSS or content into the application.
Affected endpoints
- https://www.dev-fe.emory.pro/ (affected application origin)
Evidence
External stylesheet link tags in the frontend HTML lacked integrity and crossorigin attributes.
Proof of Concept
- Inspect the HTML and observe external stylesheet link tags lack integrity and crossorigin attributes.
Impact
A compromise of or MITM against the third-party stylesheet origin could inject attacker-controlled CSS or content into the application, a style and content-injection vector. Exploitation required third-party or MITM compromise, keeping likelihood low.
Remediation
- Add SRI integrity hashes (and crossorigin) to all externally-hosted resources or self-host them, paired with a strict CSP.
Retest
Retested: FIXED — frontend CSS is now self-hosted and same-origin with no external stylesheet links.
L015 · Improper Input Validation (Data Integrity) — OWASP API6 · CWE-20
Task-Template Input-Validation Gaps (Negative Sequence, Duplicate IDs)
Description
POST /tasks/templates accepts negative field sequence values (for example -5, persisted verbatim) and duplicate item_type_ids in a single template (for example [586,586], both stored). These are data-quality and integrity gaps with no direct security impact (worst case UI ordering glitches), but they reflect missing server-side validation and are worth tightening.
Affected endpoints
- POST https://dev-be.emory.pro/tasks/templates
Evidence
POST /tasks/templates with duplicate item_type_ids [586,586] and sequence -5 returned 201 with both stored verbatim.
Proof of Concept
- POST /tasks/templates with item_type_ids [586,586] and a field sequence -5 returns 201, and GET shows duplicate item-type rows and sequence -5 persisted.
Impact
Missing input validation allowed malformed templates (negative sequence, duplicate item-type references) to persist, a data-integrity gap with no direct security impact beyond possible UI ordering glitches.
Remediation
- Validate sequence at least 0 and deduplicate item_type_ids with a unique constraint, rejecting malformed templates with 400.
Retest
Retested: FIXED — item_type_ids [586,586] plus sequence -5 now returns 400 (duplicate value and sequence must be at least 0).
L016 · Improper Neutralization of Input (Stored, Latent) — Stored XSS (unconfirmed sink) · CWE-79
Stored Unsanitized HTML in Task-Template Name
Description
POST /tasks/templates stores a template name containing raw HTML (for example an svg onload payload) and returns it unescaped in the JSON API response. In the JSON context this is inert; it becomes stored XSS only if the frontend renders the template name through an unescaped DOM sink. The value is persisted unsanitized, which is the precondition for stored XSS, flagged Low pending confirmation of a rendering sink in the UI.
Affected endpoints
- POST https://dev-be.emory.pro/tasks/templates
Evidence
POST /tasks/templates stored an svg onload template name and GET returned the raw HTML unescaped in the JSON response.
Proof of Concept
- POST /tasks/templates with a name containing an svg onload payload returns 201, and GET /tasks/templates/{id} returns the raw payload unescaped.
Impact
Unsanitized HTML persisted in the template name, the precondition for stored XSS if the frontend ever rendered it through an unescaped DOM sink. In the JSON API response it was inert, so the risk was latent pending a confirmed UI sink.
Remediation
- Server-side sanitize or encode user-supplied text fields on output, ensure the frontend renders names via text nodes (React default) and never dangerouslySetInnerHTML, and add a strict CSP as defence-in-depth.
Retest
Retested: FIXED — a template name with an svg onload payload is now stored as plain text with HTML stripped server-side.
L017 · Weak Password Requirements (Broken Authentication) — OWASP API2 · CWE-521
Weak Password Policy
Description
The password set and reset flow enforces only a minimum length of eight characters, with no complexity, dictionary or breached-password checks. Users can therefore set weak or previously-breached passwords (for example password), which materially increases the success rate of the credential-stuffing and brute-force attacks made practical by the absence of login rate-limiting (F15) and the username-enumeration oracle (F14).
Affected endpoints
- POST https://dev-be.emory.pro/user/update-password/{token}
Evidence
A password reset accepted a new password of password (8 characters, no complexity) with no policy error.
Proof of Concept
- Trigger a password reset and submit a new password of password (8 characters, no complexity) and observe it is accepted with no policy error.
Impact
Weak and previously-breached passwords were accepted, raising the likelihood of successful credential-stuffing and brute-force attacks, especially chained with the missing login rate limiting and the enumeration oracle. It weakened account-takeover resistance across the platform.
Remediation
- Enforce a stronger policy (for example minimum 12 characters with complexity, or NIST-style length-plus-breach-list checks using a service such as the Have I Been Pwned k-anonymity API) and reject common or breached passwords.
Retest
Retested: FIXED — the policy now requires at least 12 characters with uppercase, lowercase, number and special character; every weak password was rejected with a specific 400 and a compliant password was accepted (200).
L018 · Insufficient Anti-Automation — OWASP API4/API6 · CWE-799
Weak Bot Protection on Signup (Invisible CAPTCHA)
Description
Organization signup is protected by Google reCAPTCHA v3, a score-based fully invisible mechanism that a normal browser auto-mints with no human interaction. Server-side verification of the token is present, but because a real (or headless-but-real) browser obtains a passing score without solving any challenge, the practical barrier against automated or bulk signups is weak. This enables automated account and organization creation (spam, resource abuse, cost amplification).
Affected endpoints
- POST https://dev-be.emory.pro/organization/signup
Evidence
An automated real browser minted an invisible reCAPTCHA v3 token that was accepted with no human challenge, allowing repeated automated signups.
Proof of Concept
- Drive POST /organization/signup from an automated real browser; the invisible reCAPTCHA v3 token is minted and accepted with no human challenge, allowing repeated automated signups.
Impact
Bulk and automated organization creation was feasible, enabling spam and resource and cost amplification. Because a scripted real browser passed reCAPTCHA v3 without interaction, the practical anti-automation barrier was weak.
Remediation
- Add a server-enforced score threshold with step-up to an interactive challenge (reCAPTCHA v2 or hCaptcha) on low scores, and apply per-IP rate limiting and email-domain reputation checks on signup.
Retest
Retested: LARGELY FIXED — per-IP Cloudflare rate limiting now returns 429 (a 30-request burst returned 27 of 429, only 3 reached the backend) and reCAPTCHA verification is enforced (a forged token fails, no account created). Residual: the mechanism is still invisible reCAPTCHA v3 with no interactive step-up, and a bogus token yields a generic 500 rather than a clean 400.
I001 · Improper Error Handling / Information Disclosure · CWE-200, CWE-754
Misleading Success Response on No-Op / Cross-Tenant Archive Writes
Description
The archive endpoint reports success even when it changes nothing. POST /item/archive/{id} returns 200 success for a non-existent item ID or for an item belonging to another tenant, because the underlying SQL UPDATE simply matches zero rows. No cross-tenant state is actually modified (the operation is correctly scoped), so there is no integrity impact, but the misleading success response is poor API hygiene. The endpoint should return 404 when no row is affected.
Affected endpoints
- POST https://dev-be.emory.pro/item/archive/{id}
Evidence
POST /item/archive/999999 with a valid token returned HTTP 200 success despite no such item existing.
Proof of Concept
- Log in as any authenticated user.
- Send POST /item/archive/999999 (a non-existent ID) with a valid token and observe HTTP 200 success despite no such item existing.
Impact
A caller received false confirmation of an action that did not occur, and inconsistent response semantics could hide defects and complicate monitoring. No state was changed and no data was exposed, so the impact was informational only.
Remediation
- Return 404 (or 403) when an archive or update affects zero rows, and reserve 200 success for operations that actually changed a record.
Retest
Retested: FIXED — POST /item/archive/999999 now returns 404 Item not found (was 200 success).
I002 · Security Misconfiguration (Latent Authorization Foot-Gun) · CWE-1320, CWE-639
Unused Tenant-ID Path Parameter on Organization Access-Flags Endpoint
Description
GET /organization/access-flags/{id} ignores the {id} path segment entirely and always returns the caller own organization access flags regardless of the value supplied. This is not currently exploitable (the response is correctly scoped to the authenticated organization) but a path parameter that looks like a tenant selector yet is silently discarded is a latent IDOR foot-gun: a future refactor that begins honoring the parameter without adding an authorization check would immediately expose cross-tenant data.
Affected endpoints
- GET https://dev-be.emory.pro/organization/access-flags/{id}
Evidence
As an Org Admin, GET /organization/access-flags/999 returned HTTP 200 with the caller own org flags, identical to the request for the own id.
Proof of Concept
- As an Org Admin, GET /organization/access-flags/999 (a foreign or arbitrary org ID) returns HTTP 200 with the caller own org flags, identical to the request for the own id.
Impact
No data was currently exposed since the response was correctly scoped, but the misleading tenant-like parameter invited a future cross-tenant authorization defect if a refactor began honoring it without an authz check. The risk was latent only.
Remediation
- Remove the unused {id} path parameter, or honor it only behind an explicit authorization check that confirms the caller may read the requested organization.
Retest
Retested: FIXED — GET /organization/access-flags/999 now returns 404 (the parameterised route was removed).
I003 · Security Misconfiguration (Error Handling) · CWE-754
Disallowed CORS Origins Receive HTTP 500 Instead of a Normal Response
Description
When a request arrives from an origin that is not on the CORS allow-list, the API responds with 500 Not allowed by CORS rather than simply returning a normal response without CORS headers. The cross-origin policy itself is implemented correctly (the allow-list is restricted to the genuine frontend origin and does not reflect arbitrary, null, or suffix-matched origins), so this is a cosmetic or error-handling issue rather than a security exposure, but surfacing CORS rejection as a server error is incorrect behavior and adds noise to error monitoring.
Affected endpoints
- https://dev-be.emory.pro/ (affected application origin)
Evidence
A request with Origin https://evil.example returned HTTP 500 Not allowed by CORS rather than a normal response without an Access-Control-Allow-Origin header.
Proof of Concept
- Send any request with Origin https://evil.example and observe HTTP 500 Not allowed by CORS rather than a normal response with no Access-Control-Allow-Origin header.
Impact
The incorrect error semantics polluted server-error metrics but caused no cross-origin data exposure, since the CORS allow-list itself was implemented correctly. The impact was cosmetic.
Remediation
- For disallowed origins, return the normal response while simply omitting the Access-Control-Allow-Origin header, and do not raise a 500.
Retest
Retested: FIXED — a disallowed Origin now returns a normal 200 with no Access-Control reflection and no 500.
I004 · Information Disclosure (Fingerprinting) · CWE-200
Server Technology Disclosure via Response Headers
Description
The frontend origin returns headers that fingerprint the hosting stack: Server AmazonS3 and Via 1.1 cloudfront.net (CloudFront). This reveals that the SPA is served from an S3 bucket behind CloudFront, giving an attacker free reconnaissance about the infrastructure. The impact is minimal on its own, but unnecessary technology disclosure is a hardening gap worth closing.
Affected endpoints
- GET https://www.dev-fe.emory.pro/
Evidence
curl -I https://www.dev-fe.emory.pro/ returned server AmazonS3 and via cloudfront.net headers.
Proof of Concept
- Run curl -I https://www.dev-fe.emory.pro/ and observe the response includes server AmazonS3 and via cloudfront.net.
Impact
The hosting stack was fingerprinted via response headers, lowering the cost of targeted attacks. Impact was reconnaissance aid only.
Remediation
- Strip or override Server, Via and similar headers at CloudFront via a Response Headers Policy.
Retest
Status: ACCEPTED RISK (client). Server technology disclosure via response headers. Client accepts the risk. Client note: we advertise that we use AWS in our marketing anyway. No remediation required per client.
I005 · Security Misconfiguration (Environment Exposure) · CWE-200, CWE-668
Public Development / Staging Environment Reachable on the Internet
Description
The development and staging environment (dev-fe.emory.pro and dev-be.emory.pro) is reachable directly from the public internet with no network-level restriction. For this authorized engagement that exposure is expected, but as a standing posture it means non-production environments (which frequently carry weaker controls, verbose errors, debug endpoints and test data) are exposed to anyone. It is noted here so the same internet-exposure is not carried into other non-production tiers.
Affected endpoints
- GET https://dev-fe.emory.pro/
- GET https://dev-be.emory.pro/health
Evidence
GET https://dev-fe.emory.pro/ and GET https://dev-be.emory.pro/health were reachable from any internet host (302 and 200).
Proof of Concept
- GET https://dev-fe.emory.pro/ and GET https://dev-be.emory.pro/health from any internet host and observe they are reachable (302 and 200).
Impact
Non-production environments, which frequently carry weaker controls and test data, were exposed to the public, increasing the attack surface for tiers that may be less hardened. For this authorized engagement the exposure was expected.
Remediation
- Restrict non-production environments behind VPN, IP allow-listing or SSO, or at minimum apply the same hardening as production.
Retest
Status: ACCEPTED RISK (client). Public development/staging environment reachable on the internet. Client accepts the risk. Client note: all security policies are the same for dev and prod. No remediation required per client.
I006 · Use of Deprecated Cryptographic Protocol — OWASP A02 · CWE-327
Deprecated TLS 1.0 / 1.1 Reported on the Backend (Unconfirmed)
Description
Both automated scans reported that the backend dev-be.emory.pro accepts deprecated TLS 1.0 and 1.1 protocols (with AES/CBC and RSA key-exchange suites). This could not be independently confirmed during consolidation because the testing host OpenSSL build cannot complete a TLS 1.0/1.1 handshake (it negotiated cipher 0000 on -tls1 and -tls1_1), which is inconclusive rather than disproving. It is recorded as a pending hardening item, neither asserted nor dismissed, and should be re-validated with a dedicated TLS scanner. The CVSS shown reflects the issue if confirmed.
Affected endpoints
- https://dev-be.emory.pro/ (affected application origin)
Evidence
nmap ssl-enum-ciphers on dev-be.emory.pro:443 offered only TLS 1.2 and TLS 1.3 with least strength A, and openssl -tls1 and -tls1_1 negotiated no legacy protocol.
Proof of Concept
- Run testssl.sh dev-be.emory.pro:443 (or sslyze) from a host with legacy-protocol support and observe whether TLS 1.0 or 1.1 handshakes complete; local openssl s_client -tls1 and -tls1_1 was inconclusive with no cipher negotiated.
Impact
If TLS 1.0/1.1 were enabled, clients could be downgraded to deprecated versions with weaker ciphers, undermining transport security. The retest confirmed only TLS 1.2/1.3 are offered, so the concern is not present.
Remediation
- Confirm with a dedicated TLS scanner; if TLS 1.0/1.1 is enabled, restrict the listener to TLS 1.2+ (preferably TLS 1.3) and disable CBC and RSA-key-exchange suites.
Retest
Retested: RESOLVED / NOT PRESENT — nmap ssl-enum-ciphers on dev-be.emory.pro:443 offers only TLS 1.2 and TLS 1.3 (no TLS 1.0/1.1, no SSLv2/3), and openssl -tls1 and -tls1_1 negotiate no legacy protocol; TLS 1.0/1.1 are disabled.
/ Sign-off
Reviewers & Signatories
Prepared by Aditya — CredShields
- ShashankCo-founder, CredShields
aditya@credshields.com
Shashank
Co-founder & CEO · CredShields
Security analyst and penetration tester with 12 years of experience in pentesting and 5 years in managing bug bounty programs. Listed in Google, Facebook, Apple, and 30+ websites' hall of fame for reporting valid security issues.
- CVE-2017-7063
- CVE-2017-7062
- CVE-2017-2458
Aditya Dixit
Audit & Research Lead · CredShields
Senior security researcher and penetration tester with 10+ years in cybersecurity. Led pentest teams at Cobalt and HackerOne; previously at Binary.com. Executed 500+ pentests across banking, fintech, payments, healthcare, telecom, and media, covering web, mobile (iOS/Android), network, cloud, code review, and smart contracts.
- OSCP
- CREST CPSA
- AWS Security Specialty
- AWS Cloud Practitioner
- Qualys VMDR