Security report for ptengelmann/PhantomOS
Hello maintainers,
I am a security researcher studying security risks in vibe-coded software. During this research, I reviewed this repository and identified the findings below. These findings were identified in commit cf30cb2. Each finding has undergone human analysis, but the report may still contain mistakes or incomplete interpretations. Please review this report and apply the necessary security fixes.
This report contains 17 confirmed findings identified during security review. Validation details below distinguish code evidence from runtime observations and note any limitations.
Summary
| ID |
Severity |
Category |
Affected area |
| SEC-001 |
High |
Cryptographic Failures |
src/app/api/connectors/shopify/callback/route.ts |
| SEC-002 |
High |
Broken Access Control |
src/app/api/ai/tagging/route.ts |
| SEC-003 |
High |
Broken Access Control |
src/app/api/ai/tagging/route.ts |
| SEC-004 |
High |
Authentication Failures |
src/app/api/connectors/shopify/auth/route.ts
src/app/api/connectors/shopify/callback/route.ts |
| SEC-005 |
High |
Security Misconfiguration |
src/app/api/connectors/[id]/route.ts
src/lib/auth/index.ts |
| SEC-006 |
High |
Broken Access Control |
src/app/api/ai/forecast/route.ts |
| SEC-007 |
High |
Broken Access Control |
src/app/api/ai/insights/route.ts |
| SEC-008 |
High |
Injection |
src/app/api/waitlist/route.ts
src/app/admin/page.tsx |
| SEC-009 |
High |
Broken Access Control |
src/app/api/connectors/shopify/auth/route.ts
src/app/api/connectors/shopify/callback/route.ts
src/components/dashboard/connector-wizard.tsx |
| SEC-010 |
High |
Broken Access Control |
middleware.ts
src/app/admin/page.tsx |
| SEC-011 |
High |
Authentication Failures |
src/app/api/settings/invite/validate/route.ts |
| SEC-012 |
High |
Authentication Failures |
src/app/api/waitlist/route.ts |
| SEC-013 |
High |
Broken Access Control |
src/app/api/connectors/shopify/sync/products/route.ts
src/app/api/connectors/shopify/sync/orders/route.ts |
| SEC-014 |
Medium |
Broken Access Control |
src/lib/rate-limit/index.ts
src/lib/audit/index.ts |
| SEC-015 |
Medium |
Insecure Design |
src/app/api/products/import/route.ts
src/app/api/sales/import/route.ts
src/app/api/settings/profile/route.ts
src/app/(dashboard)/settings/page.tsx |
| SEC-016 |
Medium |
Security Logging and Alerting Failures |
src/app/api/settings/invite/route.ts |
| SEC-017 |
Low |
Security Logging and Alerting Failures |
src/app/api/settings/invite/route.ts
src/app/api/waitlist/approve/route.ts |
1. Encrypt Shopify Access Token Before Persisting to Database
ID: SEC-001
Severity: High
Category: Cryptographic Failures
Affected code: src/app/api/connectors/shopify/callback/route.ts
Impact
Plaintext Shopify access tokens stored in the database can be exposed through database reads, backups, logs, or a database breach and then used to access connected Shopify stores.
Technical details
- src/app/api/connectors/shopify/callback/route.ts stores credentials as { accessToken }.
- The callback does not import or call encryptCredentials.
- The code contains a comment stating encryption is intended for production, while sync code expects encrypted credentials.
Relevant code
src/app/api/connectors/shopify/callback/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { connectors } from '@/lib/db/schema';
import crypto from 'crypto';
const SHOPIFY_API_KEY = process.env.SHOPIFY_API_KEY || '';
const SHOPIFY_API_SECRET = process.env.SHOPIFY_API_SECRET || '';
// Verify Shopify HMAC signature
function verifyHmac(query: URLSearchParams): boolean {
const hmac = query.get('hmac');
if (!hmac) return false;
// Remove hmac from query params for verification
const params = new URLSearchParams(query);
params.delete('hmac');
// Sort params and create message
const sortedParams = Array.from(params.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join('&');
const generatedHmac = crypto
.createHmac('sha256', SHOPIFY_API_SECRET)
.update(sortedParams)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(hmac),
Validation
Code evidence
-
callback/route.ts stores credentials: { accessToken } directly.
-
The callback has no encryptCredentials import or invocation.
Limitations
-
No database or token-exfiltration probe was performed; the finding is confirmed from source code.
Recommended remediation
Encrypt the Shopify access token with the existing encryption utility before persistence, and ensure all read paths decrypt it only when required.
2. Add Authentication and Rate Limiting to Bulk AI Tagging PUT Handler
ID: SEC-002
Severity: High
Category: Broken Access Control
Affected code: src/app/api/ai/tagging/route.ts
Impact
The PUT tagging path can invoke the paid Anthropic API without the publisher resolution and AI rate limit used by the sibling POST path. This permits authenticated non-publisher users to cause unbounded API expenditure and may expose cross-tenant mapping data.
Technical details
- The PUT handler omits resolvePublisher() and rateLimit('ai').
- The handler calls getConfirmedMappingExamples(undefined, 20), which queries mappings without a publisher filter.
- The middleware blocks unauthenticated requests, but session authentication alone does not establish a publisher for this path.
Relevant code
src/app/api/ai/tagging/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import Anthropic from '@anthropic-ai/sdk';
import { resolvePublisher } from '@/lib/auth';
import { db } from '@/lib/db';
import { sql } from 'drizzle-orm';
import { rateLimit } from '@/lib/rate-limit';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
// Fetch confirmed mappings to use as few-shot examples
async function getConfirmedMappingExamples(category?: string, limit: number = 15): Promise<string> {
try {
// Get confirmed mappings from across all publishers (the network effect!)
const examples = await db.execute(sql`
SELECT
p.name as product_name,
p.category as product_category,
ia.name as asset_name,
ia.asset_type
FROM product_assets pa
JOIN products p ON p.id = pa.product_id
JOIN ip_assets ia ON ia.id = pa.asset_id
ORDER BY
CASE WHEN p.category = ${category || ''} THEN 0 ELSE 1 END,
pa.created_at DESC
LIMIT ${limit}
`);
Validation
Code evidence
-
src/app/api/ai/tagging/route.ts imports resolvePublisher and rateLimit, but the PUT path does not invoke them.
-
The POST path applies both controls.
Runtime evidence
-
An unauthenticated PUT to /api/ai/tagging redirected to /login.
Limitations
-
The supplied runtime check did not exercise an authenticated non-publisher session or confirm an Anthropic charge.
Recommended remediation
Require resolvePublisher() and rateLimit('ai') before processing PUT requests, and scope all example queries to the resolved publisher.
3. Cross-Tenant Data Leak: Competitor Product Names Included in AI Prompts
ID: SEC-003
Severity: High
Category: Broken Access Control
Affected code: src/app/api/ai/tagging/route.ts
Impact
Authenticated publishers may have competitor product and asset names included in their AI prompts, disclosing tenant data to another tenant and to the configured AI provider.
Technical details
- getConfirmedMappingExamples() joins product_assets, products, and ip_assets without a WHERE publisher_id predicate.
- The returned names are inserted into the prompt as LEARNED PATTERNS.
- The query therefore selects mappings across publishers rather than only the caller's tenant.
Relevant code
src/app/api/ai/tagging/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import Anthropic from '@anthropic-ai/sdk';
import { resolvePublisher } from '@/lib/auth';
import { db } from '@/lib/db';
import { sql } from 'drizzle-orm';
import { rateLimit } from '@/lib/rate-limit';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
// Fetch confirmed mappings to use as few-shot examples
async function getConfirmedMappingExamples(category?: string, limit: number = 15): Promise<string> {
try {
// Get confirmed mappings from across all publishers (the network effect!)
const examples = await db.execute(sql`
SELECT
p.name as product_name,
p.category as product_category,
ia.name as asset_name,
ia.asset_type
FROM product_assets pa
JOIN products p ON p.id = pa.product_id
JOIN ip_assets ia ON ia.id = pa.asset_id
ORDER BY
CASE WHEN p.category = ${category || ''} THEN 0 ELSE 1 END,
pa.created_at DESC
LIMIT ${limit}
`);
Validation
Code evidence
-
src/app/api/ai/tagging/route.ts contains a raw query with no publisher_id filter.
-
The function is used to build AI tagging context.
Runtime evidence
-
A POST to /api/ai/tagging without authentication redirected to /login.
Limitations
-
The supplied runtime evidence did not demonstrate a cross-tenant response using two authenticated tenants.
Recommended remediation
Pass the resolved publisher ID into getConfirmedMappingExamples() and add a tenant predicate to the query before using results in prompts.
4. Shopify OAuth State Unsigned and Nonce Unverified — CSRF and Session Fixation Risk
ID: SEC-004
Severity: High
Category: Authentication Failures
Affected code: src/app/api/connectors/shopify/auth/route.ts, src/app/api/connectors/shopify/callback/route.ts
Impact
Shopify OAuth state is not bound to the initiating session and the nonce is not verified. An attacker may forge state values, including publisherId, creating CSRF and account-association risk. HMAC verification also fails open when SHOPIFY_API_SECRET is unset.
Technical details
- The auth route encodes nonce, publisherId, and shop as plain base64 JSON.
- The nonce is generated but not stored server-side.
- The callback decodes publisherId without session or nonce binding.
- The callback checks HMAC only when SHOPIFY_API_SECRET is non-empty.
Relevant code
src/app/api/connectors/shopify/auth/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession, isDemoMode, getDemoPublisherId, canWrite } from '@/lib/auth';
import crypto from 'crypto';
// Shopify OAuth configuration
const SHOPIFY_API_KEY = process.env.SHOPIFY_API_KEY || '';
const SHOPIFY_SCOPES = 'read_products,read_orders,read_customers';
// Generate a random nonce for CSRF protection
function generateNonce(): string {
return crypto.randomBytes(16).toString('hex');
}
// Initiate Shopify OAuth flow
export async function POST(request: NextRequest) {
try {
// SECURITY: Require write access (owner/admin only)
let publisherId: string;
const session = await getServerSession();
if (session?.user?.publisherId) {
// User is logged in - always check RBAC regardless of demo mode
if (!canWrite(session.user.role)) {
return NextResponse.json({ error: 'Write access required' }, { status: 403 });
}
publisherId = session.user.publisherId;
} else if (isDemoMode()) {
// No session but demo mode - allow anonymous access
publisherId = getDemoPublisherId();
Validation
Code evidence
-
auth/route.ts creates state with Buffer.from(JSON.stringify({ nonce, publisherId, shop })).toString('base64').
-
callback/route.ts uses if (SHOPIFY_API_SECRET && !verifyHmac(searchParams)).
Runtime evidence
-
An unauthenticated callback request redirected to /login.
Limitations
-
The supplied probe did not complete an OAuth flow or demonstrate connector creation.
Recommended remediation
Bind state to the initiating session, authenticate and verify its integrity, persist and validate the nonce, and reject callbacks when the Shopify secret is absent.
5. Add Production Safeguard to Prevent Demo Mode Bypassing Authentication
ID: SEC-005
Severity: High
Category: Security Misconfiguration
Affected code: src/app/api/connectors/[id]/route.ts, src/lib/auth/index.ts
Impact
Setting DEMO_MODE=true in production can bypass authentication for routes that use the demo branch, expose connector data, and permit unauthenticated operations against the fixed demo publisher.
Technical details
- isDemoMode() is an environment-variable check with no production guard.
- GET /api/connectors/[id] selects the demo publisher when no session exists and demo mode is enabled.
- DEMO_MODE is absent from .env.example.
Relevant code
src/app/api/connectors/[id]/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { connectors, products, sales, productAssets } from '@/lib/db/schema';
import { eq, and, inArray } from 'drizzle-orm';
import { getServerSession, isDemoMode, getDemoPublisherId, canWrite } from '@/lib/auth';
// DELETE - Disconnect/delete a connector and its associated data
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: connectorId } = await params;
let publisherId: string;
// SECURITY: Require write access (owner/admin only)
const session = await getServerSession();
if (session?.user?.publisherId) {
// User is logged in - always check RBAC regardless of demo mode
if (!canWrite(session.user.role)) {
return NextResponse.json({ error: 'Write access required' }, { status: 403 });
}
publisherId = session.user.publisherId;
} else if (isDemoMode()) {
// No session but demo mode - allow anonymous access
publisherId = getDemoPublisherId();
} else {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
Validation
Code evidence
-
src/app/api/connectors/[id]/route.ts checks isDemoMode() before returning unauthorized.
-
The demo branch assigns getDemoPublisherId() without requiring a session.
Runtime evidence
-
The deployed application redirected an unauthenticated connector request to /login, indicating demo mode was not active in the tested deployment.
Limitations
-
The deployed environment was not tested with DEMO_MODE=true.
Recommended remediation
Add a startup assertion that rejects DEMO_MODE=true when NODE_ENV=production, and document the setting and its security implications.
6. IDOR in AI Forecast Endpoint Allows Cross-Tenant Data Access
ID: SEC-006
Severity: High
Category: Broken Access Control
Affected code: src/app/api/ai/forecast/route.ts
Impact
An authenticated user can request forecast data for another tenant's product or asset by supplying its identifier, potentially retrieving foreign sales history and product details.
Technical details
- The handler resolves publisherId but does not use it in productId or assetId query branches.
- Product-specific sales queries filter by product_id only.
- Asset-specific queries similarly omit publisher_id.
- The fallback branch does apply publisher scoping, creating inconsistent tenant enforcement.
Relevant code
src/app/api/ai/forecast/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { predictDemand } from '@/lib/ai';
import { db } from '@/lib/db';
import { sql } from 'drizzle-orm';
import { resolvePublisher } from '@/lib/auth';
import { rateLimit } from '@/lib/rate-limit';
export async function POST(request: NextRequest) {
try {
// Rate limit AI endpoints (expensive operations)
const rateLimitResponse = await rateLimit('ai');
if (rateLimitResponse) return rateLimitResponse;
// SECURITY: Session-first pattern - always check auth before demo mode
const resolved = await resolvePublisher();
if (!resolved) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { publisherId } = resolved;
const body = await request.json();
const { productId, assetId } = body;
// Build the query based on whether we're forecasting for a product or asset
let salesQuery;
let entityData;
if (productId) {
// Product-specific forecast
salesQuery = sql`
Validation
Code evidence
-
src/app/api/ai/forecast/route.ts obtains publisherId through resolvePublisher().
-
The productId and assetId SQL branches use identifier predicates without publisher_id.
Runtime evidence
-
An unauthenticated POST to /api/ai/forecast returned a 307 authentication redirect.
Limitations
-
No authenticated cross-tenant identifier was used in the supplied probe.
Recommended remediation
Add publisher ownership predicates or tenant-scoped joins to every productId and assetId query, and reject identifiers not owned by the resolved publisher.
7. IDOR on AI Insights PATCH Allows Cross-Tenant State Manipulation
ID: SEC-007
Severity: High
Category: Broken Access Control
Affected code: src/app/api/ai/insights/route.ts
Impact
An authenticated user can modify or dismiss another tenant's AI insight by supplying its identifier, causing cross-tenant state manipulation.
Technical details
- The PATCH handler checks that a session publisher exists but does not use publisherId in the update predicate.
- The update matches only aiInsights.id = insightId.
- A foreign insight ID can therefore reach the write operation if known.
Relevant code
src/app/api/ai/insights/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { generateInsights } from '@/lib/ai';
import { db } from '@/lib/db';
import { sql, eq, desc, isNull, and } from 'drizzle-orm';
import { resolvePublisher, getServerSession } from '@/lib/auth';
import { aiInsights, gameIps } from '@/lib/db/schema';
import { v4 as uuidv4 } from 'uuid';
import { rateLimit } from '@/lib/rate-limit';
import { audit } from '@/lib/audit';
// GET: Retrieve stored insights with history support
export async function GET(request: NextRequest) {
try {
// SECURITY: Session-first pattern
const resolved = await resolvePublisher();
if (!resolved) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { publisherId } = resolved;
// Get query params
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get('limit') || '50');
const includeHistory = searchParams.get('history') === 'true';
const compareBatchA = searchParams.get('compareA');
const compareBatchB = searchParams.get('compareB');
// Fetch all insights from database
const insights = await db
.select({
Validation
Code evidence
-
src/app/api/ai/insights/route.ts PATCH updates aiInsights using only eq(aiInsights.id, insightId).
-
session.user.publisherId is available during the handler but is absent from the update condition.
Runtime evidence
-
An unauthenticated PATCH request redirected to /login.
Limitations
-
The supplied runtime check did not perform an authenticated cross-tenant update.
Recommended remediation
Include the resolved publisher ID in the update predicate, or first load and verify ownership before applying the change.
8. Stored XSS via Unvalidated companyWebsite URL Rendered in Admin Panel
ID: SEC-008
Severity: High
Category: Injection
Affected code: src/app/api/waitlist/route.ts, src/app/admin/page.tsx
Impact
A public waitlist submission can store a javascript: URL that executes JavaScript when an administrator follows the rendered link.
Technical details
- The waitlist endpoint stores companyWebsite without validating its scheme.
- The admin page renders the value directly in an anchor href.
- The attacker-controlled value therefore flows from public input to a privileged admin view.
Relevant code
src/app/api/waitlist/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { waitlist } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { rateLimit } from '@/lib/rate-limit';
import { createAuditLog } from '@/lib/audit';
import { sendWaitlistNotificationToAdmin, isEmailConfigured } from '@/lib/email';
// POST - Add to waitlist
export async function POST(request: NextRequest) {
try {
// Rate limit public submissions (strict - 3 per minute)
const rateLimitResponse = await rateLimit('public');
if (rateLimitResponse) return rateLimitResponse;
const body = await request.json();
const { email, companyName, companyWebsite, revenueRange, primaryChannel } = body;
if (!email) {
return NextResponse.json(
{ error: 'Email is required' },
{ status: 400 }
);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: 'Please enter a valid email address' },
Validation
Code evidence
-
src/app/api/waitlist/route.ts stores companyWebsite without URL scheme validation.
-
src/app/admin/page.tsx renders entry.companyWebsite as an anchor href.
Runtime evidence
-
A POST with companyWebsite set to javascript:alert(1) returned HTTP 200 and persisted an entry.
Limitations
-
The supplied runtime evidence confirms persistence but does not document execution in an administrator browser.
Recommended remediation
Allow only approved URL schemes and validate URLs server-side before storage; also apply safe scheme validation when rendering links.
9. Fix SSRF and Open Redirect via Weak Shopify Shop Domain Validation
ID: SEC-009
Severity: High
Category: Broken Access Control
Affected code: src/app/api/connectors/shopify/auth/route.ts, src/app/api/connectors/shopify/callback/route.ts, src/components/dashboard/connector-wizard.tsx
Impact
Weak Shopify domain validation can redirect users to attacker-controlled hosts and may cause the callback to send Shopify access credentials to an unintended endpoint.
Technical details
- The auth route accepts shop values when they merely include .myshopify.com.
- A value such as evil.com.myshopify.com.attacker.com passes that check.
- The callback uses the shop parameter in https://${shop}/admin/oauth/access_token.
- The client assigns the returned authUrl directly to window.location.href.
Relevant code
src/app/api/connectors/shopify/auth/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession, isDemoMode, getDemoPublisherId, canWrite } from '@/lib/auth';
import crypto from 'crypto';
// Shopify OAuth configuration
const SHOPIFY_API_KEY = process.env.SHOPIFY_API_KEY || '';
const SHOPIFY_SCOPES = 'read_products,read_orders,read_customers';
// Generate a random nonce for CSRF protection
function generateNonce(): string {
return crypto.randomBytes(16).toString('hex');
}
// Initiate Shopify OAuth flow
export async function POST(request: NextRequest) {
try {
// SECURITY: Require write access (owner/admin only)
let publisherId: string;
const session = await getServerSession();
if (session?.user?.publisherId) {
// User is logged in - always check RBAC regardless of demo mode
if (!canWrite(session.user.role)) {
return NextResponse.json({ error: 'Write access required' }, { status: 403 });
}
publisherId = session.user.publisherId;
} else if (isDemoMode()) {
// No session but demo mode - allow anonymous access
publisherId = getDemoPublisherId();
Validation
Code evidence
-
auth/route.ts uses shopDomain.includes('.myshopify.com').
-
callback/route.ts constructs a fetch target from the shop parameter.
-
connector-wizard.tsx navigates unconditionally to data.authUrl.
Limitations
-
No crafted Shopify domain was exercised against the deployed application.
Recommended remediation
Parse and strictly validate the shop hostname against an anchored Shopify-domain rule before generating or fetching URLs, and validate the destination hostname before client navigation.
10. Admin UI Served to All Authenticated Users Regardless of Role
ID: SEC-010
Severity: High
Category: Broken Access Control
Affected code: middleware.ts, src/app/admin/page.tsx
Impact
Any authenticated role can load the admin page shell, exposing waitlist-management controls and internal UI structure even though some backend admin APIs enforce roles.
Technical details
- The middleware authorizes protected routes with !!token and does not apply a role check to /admin.
- src/app/admin/page.tsx is a client component with no server-side role check.
- The waitlist admin API separately enforces owner/admin access, limiting direct API data exposure.
Relevant code
middleware.ts:62-74
// but we still need to allow the route check
if (isAuthRoute) {
return true;
}
// All dashboard routes require authentication
return !!token;
},
},
pages: {
signIn: '/login',
},
}
Validation
Code evidence
-
middleware.ts returns !!token for dashboard routes.
-
The supplied admin page snippet contains no session or role enforcement.
Runtime evidence
-
An unauthenticated GET to /admin redirected to /login.
Limitations
-
No authenticated member or analyst session was supplied to verify rendered page access.
Recommended remediation
Enforce owner/admin authorization for the /admin route and page rendering, not only for the backing API endpoints.
11. Invited Users Inserted Without Password Hash — Permanent Authentication Failure
ID: SEC-011
Severity: High
Category: Authentication Failures
Affected code: src/app/api/settings/invite/validate/route.ts
Impact
Users created through invitation validation have no password credential and cannot authenticate through the password-based login flow.
Technical details
- The invite-validation handler has the bcrypt hashing operation commented out.
- The users insert omits passwordHash.
- The authorization callback rejects users when passwordHash is absent.
Relevant code
src/app/api/settings/invite/validate/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { invitations, users, publishers } from '@/lib/db/schema';
import { eq, and, gt } from 'drizzle-orm';
// Validate an invitation token (GET)
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const token = searchParams.get('token');
if (!token) {
return NextResponse.json(
{ error: 'Token is required' },
{ status: 400 }
);
}
// Find the invitation
const [invitation] = await db
.select({
id: invitations.id,
email: invitations.email,
name: invitations.name,
role: invitations.role,
publisherId: invitations.publisherId,
status: invitations.status,
expiresAt: invitations.expiresAt,
})
.from(invitations)
Validation
Code evidence
-
src/app/api/settings/invite/validate/route.ts inserts invited users without passwordHash.
-
src/lib/auth/index.ts returns null when user.passwordHash is missing.
Limitations
-
No invitation was created and tested through login; the failure follows directly from the source paths.
Recommended remediation
Hash the supplied password with the project's password-hashing mechanism and persist passwordHash before creating the invited user.
12. Admin Endpoint Authenticated via Plaintext Query-String Secret
ID: SEC-012
Severity: High
Category: Authentication Failures
Affected code: src/app/api/waitlist/route.ts
Impact
The waitlist administrative endpoint places the authentication secret in URLs, where it may be retained in browser history, access logs, proxy logs, or monitoring systems. Disclosure permits waitlist access.
Technical details
- The GET handler reads the key query parameter and compares it directly with ADMIN_SECRET_KEY.
- A separate session-authenticated /api/waitlist/admin endpoint exists.
- The query-string design exposes the secret outside the request body or authorization context.
Relevant code
src/app/api/waitlist/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { waitlist } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { rateLimit } from '@/lib/rate-limit';
import { createAuditLog } from '@/lib/audit';
import { sendWaitlistNotificationToAdmin, isEmailConfigured } from '@/lib/email';
// POST - Add to waitlist
export async function POST(request: NextRequest) {
try {
// Rate limit public submissions (strict - 3 per minute)
const rateLimitResponse = await rateLimit('public');
if (rateLimitResponse) return rateLimitResponse;
const body = await request.json();
const { email, companyName, companyWebsite, revenueRange, primaryChannel } = body;
if (!email) {
return NextResponse.json(
{ error: 'Email is required' },
{ status: 400 }
);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: 'Please enter a valid email address' },
Validation
Code evidence
-
src/app/api/waitlist/route.ts reads ?key= and compares it with process.env.ADMIN_SECRET_KEY.
Runtime evidence
-
GET /api/waitlist?key= returned 401, showing the endpoint is live and uses this gate.
Limitations
-
The supplied probe did not test a valid leaked secret or retrieve waitlist records.
Recommended remediation
Remove the query-secret handler and use the existing session-authenticated administrative endpoint with role enforcement.
13. Validate Shopify Pagination Link Header URL Before Following
ID: SEC-013
Severity: High
Category: Broken Access Control
Affected code: src/app/api/connectors/shopify/sync/products/route.ts, src/app/api/connectors/shopify/sync/orders/route.ts
Impact
A malicious Shopify response can redirect pagination to an attacker-controlled or internal URL, causing the server to forward the Shopify access token in a request header.
Technical details
- Product and order sync handlers extract the next URL from the Shopify Link header.
- The extracted URL is used without validating its origin.
- The subsequent fetch includes X-Shopify-Access-Token.
Relevant code
src/app/api/connectors/shopify/sync/products/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { connectors, products } from '@/lib/db/schema';
import { eq, and, inArray } from 'drizzle-orm';
import { resolvePublisherWithWriteAccess } from '@/lib/auth';
import { decryptCredentials } from '@/lib/crypto';
import { audit } from '@/lib/audit';
import { rateLimit } from '@/lib/rate-limit';
interface ShopifyProduct {
id: number;
title: string;
body_html: string;
vendor: string;
product_type: string;
tags: string;
variants: Array<{
id: number;
sku: string;
price: string;
}>;
images: Array<{
src: string;
}>;
}
// Category mapping for Shopify product types
const categoryMap: Record<string, 'apparel' | 'collectibles' | 'accessories' | 'home' | 'digital' | 'other'> = {
clothing: 'apparel',
apparel: 'apparel',
Validation
Code evidence
-
Both sync handlers assign the matched Link URL directly to nextPageUrl.
-
Both then call fetch(nextPageUrl, { headers: { 'X-Shopify-Access-Token': credentials.accessToken } }).
Limitations
-
A live exploit requires a Shopify shop or upstream response that returns a crafted Link header; no such response was tested.
Recommended remediation
Parse each pagination URL and require it to match the expected Shopify shop origin before following it or forwarding credentials.
14. Rate Limiting and Audit Logging Trust Attacker-Controlled X-Forwarded-For Header
ID: SEC-014
Severity: Medium
Category: Broken Access Control
Affected code: src/lib/rate-limit/index.ts, src/lib/audit/index.ts
Impact
If clients can supply the first X-Forwarded-For value, attackers can rotate the rate-limit key and falsify IP fields in audit records.
Technical details
- The rate limiter selects the first comma-separated X-Forwarded-For value before x-real-ip.
- The audit logger uses the same header precedence.
- Neither path shown validates that X-Forwarded-For was inserted by a trusted proxy.
Relevant code
src/lib/rate-limit/index.ts:106-118
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
// Get IP from headers (Vercel/Cloudflare)
const ip = forwardedFor?.split(',')[0]?.trim() || realIp || 'anonymous';
return `ip:${ip}`;
}
// Rate limit check function
export async function checkRateLimit(
type: RateLimitType = 'read',
Validation
Code evidence
-
src/lib/rate-limit/index.ts uses forwardedFor?.split(',')[0]?.trim() || realIp.
-
The supplied record reports the same extraction logic in src/lib/audit/index.ts.
Runtime evidence
-
The supplied runtime check reports that /api/auth/login accepted a request containing X-Forwarded-For.
Limitations
-
The record does not establish whether the deployment's edge rewrites or strips this header for every request path.
Recommended remediation
Use only proxy-authenticated client-IP metadata, configure the deployment proxy to strip client-supplied forwarding headers, and otherwise avoid using untrusted X-Forwarded-For values.
15. Enforce Server-Side File Size, MIME, and Content Validation on Upload Endpoints
ID: SEC-015
Severity: Medium
Category: Insecure Design
Affected code: src/app/api/products/import/route.ts, src/app/api/sales/import/route.ts, src/app/api/settings/profile/route.ts, src/app/(dashboard)/settings/page.tsx
Impact
Authenticated users can submit arbitrarily large CSV files that are fully buffered in memory, creating memory-exhaustion risk. Avatar values are stored without validation and rendered to team members, enabling tracking URLs and unsafe image schemes.
Technical details
- CSV import code calls file.text() without a prior size or MIME check.
- The profile endpoint persists avatar as an arbitrary string.
- The profile UI renders the value as an image source.
- The supplied finding applies the same CSV concern to the sales import path.
Relevant code
src/app/api/products/import/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { products } from '@/lib/db/schema';
import { getServerSession, isDemoMode, getDemoPublisherId, requireWriteAccess, canWrite } from '@/lib/auth';
interface CSVProduct {
name: string;
sku?: string;
description?: string;
category?: string;
price?: string;
vendor?: string;
tags?: string;
externalId?: string;
}
// Import products from CSV
export async function POST(request: NextRequest) {
try {
// SECURITY: Get publisherId from session, not form data
// SECURITY: Require write access (owner/admin only)
let publisherId: string;
const session = await getServerSession();
if (session?.user?.publisherId) {
// User is logged in - always check RBAC regardless of demo mode
if (!canWrite(session.user.role)) {
return NextResponse.json({ error: 'Write access required' }, { status: 403 });
}
Validation
Code evidence
-
src/app/api/products/import/route.ts calls file.text() without a file.size check.
-
src/app/api/settings/profile/route.ts assigns avatar without length, scheme, or content validation.
Runtime evidence
-
The supplied checks reached auth-gated import and profile endpoints and returned 307/405 responses.
Limitations
-
No oversized upload or unsafe avatar was submitted in the supplied runtime checks.
Recommended remediation
Enforce server-side upload size and type limits, use streaming CSV parsing, validate content, and restrict avatar URLs and formats while rejecting unsafe schemes and SVG where appropriate.
16. Remove Invite Security Token From Server Console Log
ID: SEC-016
Severity: Medium
Category: Security Logging and Alerting Failures
Affected code: src/app/api/settings/invite/route.ts
Impact
Invite tokens are written to server logs in full. Anyone with access to the application log stream can use a still-valid token to accept an invitation, potentially including invitations with administrative roles.
Technical details
- Invite creation builds an invite URL containing the raw 64-character token.
- The handler logs the complete URL with console.log.
- The existing email delivery path does not prevent the token from also entering logs.
Relevant code
src/app/api/settings/invite/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { invitations, users } from '@/lib/db/schema';
import { eq, and, gt } from 'drizzle-orm';
import { getServerSession, isDemoMode, getDemoPublisherId } from '@/lib/auth';
import crypto from 'crypto';
// Generate a secure random token
function generateToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// Create a new invitation
export async function POST(request: NextRequest) {
try {
// SECURITY: Get session and verify admin role
let publisherId: string;
let inviterId: string | null = null;
if (isDemoMode()) {
publisherId = getDemoPublisherId();
} else {
const session = await getServerSession();
if (!session?.user?.publisherId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Check if user has permission to invite (owner or admin)
const userRole = session.user.role;
if (!['owner', 'admin'].includes(userRole)) {
Validation
Code evidence
-
src/app/api/settings/invite/route.ts logs Invite URL for ${email}: ${inviteUrl}.
-
The logged inviteUrl embeds the raw invitation token.
Runtime evidence
-
The supplied endpoint check returned a 307 authentication redirect; logging occurs server-side after an authorized request.
Limitations
-
The record does not show whether a specific deployment log provider retained or exposed a token.
Recommended remediation
Remove the full invite URL and token from logs and deliver invitation links through the existing email mechanism without logging secret values.
17. Remove Raw Security Tokens From HTTP Response Bodies
ID: SEC-017
Severity: Low
Category: Security Logging and Alerting Failures
Affected code: src/app/api/settings/invite/route.ts, src/app/api/waitlist/approve/route.ts
Impact
Raw invitation tokens in HTTP responses can be captured by proxies, CDNs, APM systems, browser tooling, or other components that record response bodies. Disclosure allows unauthorized invitation acceptance.
Technical details
- The settings invite response includes inviteUrl containing the raw token.
- The waitlist approval response includes inviteToken and inviteLink.
- Both endpoints are intended for administrative users, but response intermediaries may still record the values.
Relevant code
src/app/api/settings/invite/route.ts:1-30
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { invitations, users } from '@/lib/db/schema';
import { eq, and, gt } from 'drizzle-orm';
import { getServerSession, isDemoMode, getDemoPublisherId } from '@/lib/auth';
import crypto from 'crypto';
// Generate a secure random token
function generateToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// Create a new invitation
export async function POST(request: NextRequest) {
try {
// SECURITY: Get session and verify admin role
let publisherId: string;
let inviterId: string | null = null;
if (isDemoMode()) {
publisherId = getDemoPublisherId();
} else {
const session = await getServerSession();
if (!session?.user?.publisherId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Check if user has permission to invite (owner or admin)
const userRole = session.user.role;
if (!['owner', 'admin'].includes(userRole)) {
Validation
Code evidence
-
src/app/api/settings/invite/route.ts returns inviteUrl containing the token.
-
src/app/api/waitlist/approve/route.ts returns inviteToken and inviteLink.
Runtime evidence
-
The supplied checks report both endpoints reject unauthenticated requests with 401.
Limitations
-
No proxy, CDN, or APM capture was demonstrated.
Recommended remediation
Return only non-sensitive status information such as emailSent, and do not include raw tokens or invitation URLs in production responses.
Security report for ptengelmann/PhantomOS
Hello maintainers,
I am a security researcher studying security risks in vibe-coded software. During this research, I reviewed this repository and identified the findings below. These findings were identified in commit
cf30cb2. Each finding has undergone human analysis, but the report may still contain mistakes or incomplete interpretations. Please review this report and apply the necessary security fixes.This report contains 17 confirmed findings identified during security review. Validation details below distinguish code evidence from runtime observations and note any limitations.
Summary
src/app/api/connectors/shopify/callback/route.tssrc/app/api/ai/tagging/route.tssrc/app/api/ai/tagging/route.tssrc/app/api/connectors/shopify/auth/route.tssrc/app/api/connectors/shopify/callback/route.tssrc/app/api/connectors/[id]/route.tssrc/lib/auth/index.tssrc/app/api/ai/forecast/route.tssrc/app/api/ai/insights/route.tssrc/app/api/waitlist/route.tssrc/app/admin/page.tsxsrc/app/api/connectors/shopify/auth/route.tssrc/app/api/connectors/shopify/callback/route.tssrc/components/dashboard/connector-wizard.tsxmiddleware.tssrc/app/admin/page.tsxsrc/app/api/settings/invite/validate/route.tssrc/app/api/waitlist/route.tssrc/app/api/connectors/shopify/sync/products/route.tssrc/app/api/connectors/shopify/sync/orders/route.tssrc/lib/rate-limit/index.tssrc/lib/audit/index.tssrc/app/api/products/import/route.tssrc/app/api/sales/import/route.tssrc/app/api/settings/profile/route.tssrc/app/(dashboard)/settings/page.tsxsrc/app/api/settings/invite/route.tssrc/app/api/settings/invite/route.tssrc/app/api/waitlist/approve/route.ts1. Encrypt Shopify Access Token Before Persisting to Database
ID:
SEC-001Severity: High
Category: Cryptographic Failures
Affected code:
src/app/api/connectors/shopify/callback/route.tsImpact
Plaintext Shopify access tokens stored in the database can be exposed through database reads, backups, logs, or a database breach and then used to access connected Shopify stores.
Technical details
Relevant code
src/app/api/connectors/shopify/callback/route.ts:1-30Validation
Code evidence
callback/route.ts stores credentials: { accessToken } directly.
The callback has no encryptCredentials import or invocation.
Limitations
No database or token-exfiltration probe was performed; the finding is confirmed from source code.
Recommended remediation
Encrypt the Shopify access token with the existing encryption utility before persistence, and ensure all read paths decrypt it only when required.
2. Add Authentication and Rate Limiting to Bulk AI Tagging PUT Handler
ID:
SEC-002Severity: High
Category: Broken Access Control
Affected code:
src/app/api/ai/tagging/route.tsImpact
The PUT tagging path can invoke the paid Anthropic API without the publisher resolution and AI rate limit used by the sibling POST path. This permits authenticated non-publisher users to cause unbounded API expenditure and may expose cross-tenant mapping data.
Technical details
Relevant code
src/app/api/ai/tagging/route.ts:1-30Validation
Code evidence
src/app/api/ai/tagging/route.ts imports resolvePublisher and rateLimit, but the PUT path does not invoke them.
The POST path applies both controls.
Runtime evidence
An unauthenticated PUT to /api/ai/tagging redirected to /login.
Limitations
The supplied runtime check did not exercise an authenticated non-publisher session or confirm an Anthropic charge.
Recommended remediation
Require resolvePublisher() and rateLimit('ai') before processing PUT requests, and scope all example queries to the resolved publisher.
3. Cross-Tenant Data Leak: Competitor Product Names Included in AI Prompts
ID:
SEC-003Severity: High
Category: Broken Access Control
Affected code:
src/app/api/ai/tagging/route.tsImpact
Authenticated publishers may have competitor product and asset names included in their AI prompts, disclosing tenant data to another tenant and to the configured AI provider.
Technical details
Relevant code
src/app/api/ai/tagging/route.ts:1-30Validation
Code evidence
src/app/api/ai/tagging/route.ts contains a raw query with no publisher_id filter.
The function is used to build AI tagging context.
Runtime evidence
A POST to /api/ai/tagging without authentication redirected to /login.
Limitations
The supplied runtime evidence did not demonstrate a cross-tenant response using two authenticated tenants.
Recommended remediation
Pass the resolved publisher ID into getConfirmedMappingExamples() and add a tenant predicate to the query before using results in prompts.
4. Shopify OAuth State Unsigned and Nonce Unverified — CSRF and Session Fixation Risk
ID:
SEC-004Severity: High
Category: Authentication Failures
Affected code:
src/app/api/connectors/shopify/auth/route.ts,src/app/api/connectors/shopify/callback/route.tsImpact
Shopify OAuth state is not bound to the initiating session and the nonce is not verified. An attacker may forge state values, including publisherId, creating CSRF and account-association risk. HMAC verification also fails open when SHOPIFY_API_SECRET is unset.
Technical details
Relevant code
src/app/api/connectors/shopify/auth/route.ts:1-30Validation
Code evidence
auth/route.ts creates state with Buffer.from(JSON.stringify({ nonce, publisherId, shop })).toString('base64').
callback/route.ts uses if (SHOPIFY_API_SECRET && !verifyHmac(searchParams)).
Runtime evidence
An unauthenticated callback request redirected to /login.
Limitations
The supplied probe did not complete an OAuth flow or demonstrate connector creation.
Recommended remediation
Bind state to the initiating session, authenticate and verify its integrity, persist and validate the nonce, and reject callbacks when the Shopify secret is absent.
5. Add Production Safeguard to Prevent Demo Mode Bypassing Authentication
ID:
SEC-005Severity: High
Category: Security Misconfiguration
Affected code:
src/app/api/connectors/[id]/route.ts,src/lib/auth/index.tsImpact
Setting DEMO_MODE=true in production can bypass authentication for routes that use the demo branch, expose connector data, and permit unauthenticated operations against the fixed demo publisher.
Technical details
Relevant code
src/app/api/connectors/[id]/route.ts:1-30Validation
Code evidence
src/app/api/connectors/[id]/route.ts checks isDemoMode() before returning unauthorized.
The demo branch assigns getDemoPublisherId() without requiring a session.
Runtime evidence
The deployed application redirected an unauthenticated connector request to /login, indicating demo mode was not active in the tested deployment.
Limitations
The deployed environment was not tested with DEMO_MODE=true.
Recommended remediation
Add a startup assertion that rejects DEMO_MODE=true when NODE_ENV=production, and document the setting and its security implications.
6. IDOR in AI Forecast Endpoint Allows Cross-Tenant Data Access
ID:
SEC-006Severity: High
Category: Broken Access Control
Affected code:
src/app/api/ai/forecast/route.tsImpact
An authenticated user can request forecast data for another tenant's product or asset by supplying its identifier, potentially retrieving foreign sales history and product details.
Technical details
Relevant code
src/app/api/ai/forecast/route.ts:1-30Validation
Code evidence
src/app/api/ai/forecast/route.ts obtains publisherId through resolvePublisher().
The productId and assetId SQL branches use identifier predicates without publisher_id.
Runtime evidence
An unauthenticated POST to /api/ai/forecast returned a 307 authentication redirect.
Limitations
No authenticated cross-tenant identifier was used in the supplied probe.
Recommended remediation
Add publisher ownership predicates or tenant-scoped joins to every productId and assetId query, and reject identifiers not owned by the resolved publisher.
7. IDOR on AI Insights PATCH Allows Cross-Tenant State Manipulation
ID:
SEC-007Severity: High
Category: Broken Access Control
Affected code:
src/app/api/ai/insights/route.tsImpact
An authenticated user can modify or dismiss another tenant's AI insight by supplying its identifier, causing cross-tenant state manipulation.
Technical details
Relevant code
src/app/api/ai/insights/route.ts:1-30Validation
Code evidence
src/app/api/ai/insights/route.ts PATCH updates aiInsights using only eq(aiInsights.id, insightId).
session.user.publisherId is available during the handler but is absent from the update condition.
Runtime evidence
An unauthenticated PATCH request redirected to /login.
Limitations
The supplied runtime check did not perform an authenticated cross-tenant update.
Recommended remediation
Include the resolved publisher ID in the update predicate, or first load and verify ownership before applying the change.
8. Stored XSS via Unvalidated companyWebsite URL Rendered in Admin Panel
ID:
SEC-008Severity: High
Category: Injection
Affected code:
src/app/api/waitlist/route.ts,src/app/admin/page.tsxImpact
A public waitlist submission can store a javascript: URL that executes JavaScript when an administrator follows the rendered link.
Technical details
Relevant code
src/app/api/waitlist/route.ts:1-30Validation
Code evidence
src/app/api/waitlist/route.ts stores companyWebsite without URL scheme validation.
src/app/admin/page.tsx renders entry.companyWebsite as an anchor href.
Runtime evidence
A POST with companyWebsite set to javascript:alert(1) returned HTTP 200 and persisted an entry.
Limitations
The supplied runtime evidence confirms persistence but does not document execution in an administrator browser.
Recommended remediation
Allow only approved URL schemes and validate URLs server-side before storage; also apply safe scheme validation when rendering links.
9. Fix SSRF and Open Redirect via Weak Shopify Shop Domain Validation
ID:
SEC-009Severity: High
Category: Broken Access Control
Affected code:
src/app/api/connectors/shopify/auth/route.ts,src/app/api/connectors/shopify/callback/route.ts,src/components/dashboard/connector-wizard.tsxImpact
Weak Shopify domain validation can redirect users to attacker-controlled hosts and may cause the callback to send Shopify access credentials to an unintended endpoint.
Technical details
Relevant code
src/app/api/connectors/shopify/auth/route.ts:1-30Validation
Code evidence
auth/route.ts uses shopDomain.includes('.myshopify.com').
callback/route.ts constructs a fetch target from the shop parameter.
connector-wizard.tsx navigates unconditionally to data.authUrl.
Limitations
No crafted Shopify domain was exercised against the deployed application.
Recommended remediation
Parse and strictly validate the shop hostname against an anchored Shopify-domain rule before generating or fetching URLs, and validate the destination hostname before client navigation.
10. Admin UI Served to All Authenticated Users Regardless of Role
ID:
SEC-010Severity: High
Category: Broken Access Control
Affected code:
middleware.ts,src/app/admin/page.tsxImpact
Any authenticated role can load the admin page shell, exposing waitlist-management controls and internal UI structure even though some backend admin APIs enforce roles.
Technical details
Relevant code
middleware.ts:62-74Validation
Code evidence
middleware.ts returns !!token for dashboard routes.
The supplied admin page snippet contains no session or role enforcement.
Runtime evidence
An unauthenticated GET to /admin redirected to /login.
Limitations
No authenticated member or analyst session was supplied to verify rendered page access.
Recommended remediation
Enforce owner/admin authorization for the /admin route and page rendering, not only for the backing API endpoints.
11. Invited Users Inserted Without Password Hash — Permanent Authentication Failure
ID:
SEC-011Severity: High
Category: Authentication Failures
Affected code:
src/app/api/settings/invite/validate/route.tsImpact
Users created through invitation validation have no password credential and cannot authenticate through the password-based login flow.
Technical details
Relevant code
src/app/api/settings/invite/validate/route.ts:1-30Validation
Code evidence
src/app/api/settings/invite/validate/route.ts inserts invited users without passwordHash.
src/lib/auth/index.ts returns null when user.passwordHash is missing.
Limitations
No invitation was created and tested through login; the failure follows directly from the source paths.
Recommended remediation
Hash the supplied password with the project's password-hashing mechanism and persist passwordHash before creating the invited user.
12. Admin Endpoint Authenticated via Plaintext Query-String Secret
ID:
SEC-012Severity: High
Category: Authentication Failures
Affected code:
src/app/api/waitlist/route.tsImpact
The waitlist administrative endpoint places the authentication secret in URLs, where it may be retained in browser history, access logs, proxy logs, or monitoring systems. Disclosure permits waitlist access.
Technical details
Relevant code
src/app/api/waitlist/route.ts:1-30Validation
Code evidence
src/app/api/waitlist/route.ts reads ?key= and compares it with process.env.ADMIN_SECRET_KEY.
Runtime evidence
GET /api/waitlist?key= returned 401, showing the endpoint is live and uses this gate.
Limitations
The supplied probe did not test a valid leaked secret or retrieve waitlist records.
Recommended remediation
Remove the query-secret handler and use the existing session-authenticated administrative endpoint with role enforcement.
13. Validate Shopify Pagination Link Header URL Before Following
ID:
SEC-013Severity: High
Category: Broken Access Control
Affected code:
src/app/api/connectors/shopify/sync/products/route.ts,src/app/api/connectors/shopify/sync/orders/route.tsImpact
A malicious Shopify response can redirect pagination to an attacker-controlled or internal URL, causing the server to forward the Shopify access token in a request header.
Technical details
Relevant code
src/app/api/connectors/shopify/sync/products/route.ts:1-30Validation
Code evidence
Both sync handlers assign the matched Link URL directly to nextPageUrl.
Both then call fetch(nextPageUrl, { headers: { 'X-Shopify-Access-Token': credentials.accessToken } }).
Limitations
A live exploit requires a Shopify shop or upstream response that returns a crafted Link header; no such response was tested.
Recommended remediation
Parse each pagination URL and require it to match the expected Shopify shop origin before following it or forwarding credentials.
14. Rate Limiting and Audit Logging Trust Attacker-Controlled X-Forwarded-For Header
ID:
SEC-014Severity: Medium
Category: Broken Access Control
Affected code:
src/lib/rate-limit/index.ts,src/lib/audit/index.tsImpact
If clients can supply the first X-Forwarded-For value, attackers can rotate the rate-limit key and falsify IP fields in audit records.
Technical details
Relevant code
src/lib/rate-limit/index.ts:106-118Validation
Code evidence
src/lib/rate-limit/index.ts uses forwardedFor?.split(',')[0]?.trim() || realIp.
The supplied record reports the same extraction logic in src/lib/audit/index.ts.
Runtime evidence
The supplied runtime check reports that /api/auth/login accepted a request containing X-Forwarded-For.
Limitations
The record does not establish whether the deployment's edge rewrites or strips this header for every request path.
Recommended remediation
Use only proxy-authenticated client-IP metadata, configure the deployment proxy to strip client-supplied forwarding headers, and otherwise avoid using untrusted X-Forwarded-For values.
15. Enforce Server-Side File Size, MIME, and Content Validation on Upload Endpoints
ID:
SEC-015Severity: Medium
Category: Insecure Design
Affected code:
src/app/api/products/import/route.ts,src/app/api/sales/import/route.ts,src/app/api/settings/profile/route.ts,src/app/(dashboard)/settings/page.tsxImpact
Authenticated users can submit arbitrarily large CSV files that are fully buffered in memory, creating memory-exhaustion risk. Avatar values are stored without validation and rendered to team members, enabling tracking URLs and unsafe image schemes.
Technical details
Relevant code
src/app/api/products/import/route.ts:1-30Validation
Code evidence
src/app/api/products/import/route.ts calls file.text() without a file.size check.
src/app/api/settings/profile/route.ts assigns avatar without length, scheme, or content validation.
Runtime evidence
The supplied checks reached auth-gated import and profile endpoints and returned 307/405 responses.
Limitations
No oversized upload or unsafe avatar was submitted in the supplied runtime checks.
Recommended remediation
Enforce server-side upload size and type limits, use streaming CSV parsing, validate content, and restrict avatar URLs and formats while rejecting unsafe schemes and SVG where appropriate.
16. Remove Invite Security Token From Server Console Log
ID:
SEC-016Severity: Medium
Category: Security Logging and Alerting Failures
Affected code:
src/app/api/settings/invite/route.tsImpact
Invite tokens are written to server logs in full. Anyone with access to the application log stream can use a still-valid token to accept an invitation, potentially including invitations with administrative roles.
Technical details
Relevant code
src/app/api/settings/invite/route.ts:1-30Validation
Code evidence
src/app/api/settings/invite/route.ts logs Invite URL for ${email}: ${inviteUrl}.
The logged inviteUrl embeds the raw invitation token.
Runtime evidence
The supplied endpoint check returned a 307 authentication redirect; logging occurs server-side after an authorized request.
Limitations
The record does not show whether a specific deployment log provider retained or exposed a token.
Recommended remediation
Remove the full invite URL and token from logs and deliver invitation links through the existing email mechanism without logging secret values.
17. Remove Raw Security Tokens From HTTP Response Bodies
ID:
SEC-017Severity: Low
Category: Security Logging and Alerting Failures
Affected code:
src/app/api/settings/invite/route.ts,src/app/api/waitlist/approve/route.tsImpact
Raw invitation tokens in HTTP responses can be captured by proxies, CDNs, APM systems, browser tooling, or other components that record response bodies. Disclosure allows unauthorized invitation acceptance.
Technical details
Relevant code
src/app/api/settings/invite/route.ts:1-30Validation
Code evidence
src/app/api/settings/invite/route.ts returns inviteUrl containing the token.
src/app/api/waitlist/approve/route.ts returns inviteToken and inviteLink.
Runtime evidence
The supplied checks report both endpoints reject unauthenticated requests with 401.
Limitations
No proxy, CDN, or APM capture was demonstrated.
Recommended remediation
Return only non-sensitive status information such as emailSent, and do not include raw tokens or invitation URLs in production responses.