Static Code Review
Security Findings Report
2026-06-09 · 8 findings
8
Total Findings
3
Critical
5
High
6
Confirmed
Quotation Request Data Isolation Breach (IDOR Vulnerability)
Not fully CONFIRMED — depends on Base44 RLS enforcement
Affected Files
base44.entities.QuotationRequest.list('-created_date', 50)
.then(setRequests)
.catch(() => setRequests([]))
.finally(() => setLoading(false));"read": {
"$or": [
{ "created_by_id": "{{user.id}}" },
{ "user_condition": { "role": "admin" } }
]
}Why It Is Vulnerable
- •Frontend calls .list() with no filter argument
- •Relies entirely on Base44 platform to enforce created_by_id RLS rule
- •No defense-in-depth: frontend has zero ownership verification
- •Inconsistency: CustomerDashboard.jsx line 373 uses safer explicit .filter() pattern
- •If platform-level RLS is bypassable, all quotations from all customers become accessible
Exploitation Scenario
Customer A calls .list() and receives Customer B's quotations including product names, quantities, pricing, and notes. Competitive intelligence theft and privacy breach.
Recommended Fix
Replace .list() with explicit .filter({ created_by_id: user.id }). Match the safer pattern already used in CustomerDashboard.jsx line 373.Customer Dashboard Route Protection Missing
Active vulnerability — no framework-level route protection
Affected Files
<Route path="/dashboard" element={<CustomerDashboard />} />
<Route path="/my-quotations" element={<MyQuotations />} />
<Route path="/profile" element={<CustomerProfile />} />useEffect(() => {
base44.auth.me()
.then((u) => {
setUser(u || null);
})
.catch(() => setUser(null))
.finally(() => setAuthChecked(true));
}, []);
if (!authChecked) return <Loader2 ... />;
if (!user) return <div>Sign in to view...</div>;Why It Is Vulnerable
- •Customer portal routes completely outside <ProtectedRoute> framework wrapper
- •Authentication checked only via base44.auth.me() inside component (asynchronous)
- •Application-level auth check does not prevent page load or rendering
- •No hard redirect to /login; shows soft 'Sign in' prompt instead
- •Comparison: admin routes correctly use <ProtectedRoute> wrapper
Exploitation Scenario
Unauthenticated user hits /dashboard directly. Page renders with loading spinner, then shows 'Sign in' prompt. No framework-level block. Cached or slow-loading page could briefly expose partial content.
Recommended Fix
Wrap all customer portal routes in <ProtectedRoute> with hard redirect: <Route element={<ProtectedRoute unauthenticatedElement={<Navigate to="/login" replace />} />}>ERP Credentials Exposure via Entity Storage
Significant architectural risk; not exploited in current codebase but high-impact if breached
Affected Files
"erpUrl": { "type": "string" },
"apiKey": { "type": "string" },
"apiSecret": { "type": "string" }const settingsList = await base44.asServiceRole.entities.ERPSettings.list();
const cfg = settingsList.find(r => r.isActive) || settingsList[0];
const authHeader = `token ${cfg.apiKey.trim()}:${cfg.apiSecret.trim()}`;const settingsList = await base44.asServiceRole.entities.ERPSettings.list();
const cfg = settingsList.find((r) => r.isActive) || settingsList[0] || null;
const authHeader = `token ${cfg.apiKey.trim()}:${cfg.apiSecret.trim()}`;Why It Is Vulnerable
- •ERP API credentials stored as plain text in database (not environment variables)
- •Any database breach or backup exposure reveals credentials directly
- •All admin users can view apiKey and apiSecret in Admin ERP Settings page UI
- •base44.asServiceRole bypasses RLS — any service-role function could access them
- •No credential rotation mechanism, expiry, or audit log for credential access
- •Against security best practice: secrets should be in environment variables, not DB
Exploitation Scenario
Admin user (legitimate or compromised via privilege escalation) accesses Admin ERP Settings page and copies apiKey and apiSecret. With these credentials, they make direct API calls to ERPNext to read all customers, invoices, delivery notes, and confidential business records outside the application.
Recommended Fix
Use environment variables instead of entity fields: const erpUrl = Deno.env.get('ERP_URL'); const apiKey = Deno.env.get('ERP_API_KEY'). Remove apiKey and apiSecret fields from ERPSettings entity entirely.Inconsistent Admin Authorization Logic
Active vulnerability — allows unauthenticated calls in retrySyncCustomer
Affected Files
const user = await base44.auth.me().catch(() => null);
if (user && user.role !== 'admin') {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
// If user is null, the if() is FALSE and execution CONTINUESconst user = await base44.auth.me();
if (!user || user.role !== 'admin') {
return Response.json({ error: 'Forbidden: Admin access required' }, { status: 403 });
}const user = await base44.auth.me();
if (!user || user.role !== 'admin') {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}Why It Is Vulnerable
- •retrySyncCustomer.js uses: if (user && user.role !== 'admin') — logical gap
- •When user is null (unauthenticated), condition evaluates to if(null && ...) = false
- •The authorization check is skipped entirely for unauthenticated requests
- •Function proceeds to call base44.asServiceRole and make live ERP API calls
- •Inconsistent: syncERPProducts.js uses correct pattern: if (!user || user.role !== 'admin')
- •Comment acknowledges intent to allow scheduled automations, but also allows any HTTP request
Exploitation Scenario
Attacker discovers the function endpoint URL and makes direct POST request without auth token. Function processes all pending SyncLog entries, makes live ERP API calls to create/update Customers, and modifies user profiles via base44.asServiceRole.
Recommended Fix
Add automation secret header validation: const automationKey = req.headers.get('x-automation-key'); if (!user && automationKey !== AUTOMATION_SECRET) { return 403; } if (user && user.role !== 'admin') { return 403; }File Upload Type Validation Bypass via Drag-and-Drop
Active vulnerability — drag-and-drop bypasses all file type restrictions
Affected Files
export default function ImageUploader({ ..., accept = '.jpg,.jpeg,.png,.webp' }) {
const handleFile = async (file) => {
if (!file) return;
setUploading(true);
const { file_url } = await base44.integrations.Core.UploadFile({ file });
onChange(file_url);
setUploading(false);
};
const handleDrop = (e) => {
e.preventDefault();
setDragOver(false);
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
};Why It Is Vulnerable
- •accept attribute is browser-side only — filters file picker dialog UI only
- •accept does NOT prevent any file from being uploaded — trivially bypassed
- •handleDrop does not check file type or size before upload
- •Any file (exe, html, svg, js, pdf) can be dragged onto the drop zone
- •handleFile has zero validation: only checks if(file) exists
- •No file.size check anywhere in the component
- •Drag-and-drop path bypasses accept attribute entirely
Exploitation Scenario
Admin user (or compromised account) drags .svg file containing embedded JavaScript onto upload zone. SVG is uploaded and stored. If served with Content-Type: image/svg+xml, script executes in any browser that renders it — stored XSS via file upload.
Recommended Fix
Add validation in handleFile: const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']; const MAX_SIZE = 5*1024*1024; if (!ALLOWED_TYPES.includes(file.type)) { toast.error('Only JPG, PNG, WebP allowed'); return; } if (file.size > MAX_SIZE) { toast.error('File under 5MB'); return; }SyncLog Entity Stores Sensitive Registration Data Indefinitely
Active vulnerability — PII stored without retention policy or minimal necessary principle
Affected Files
"registration_data": {
"type": "string",
"description": "JSON-encoded registration payload for retry"
}registration_data: JSON.stringify(payload),
await base44.asServiceRole.entities.SyncLog.create({
user_email: email,
user_id: user.id,
customer_name: companyName,
customer_type: customerType || 'Company',
business_type: businessType || '',
phone: phone || '',
erp_customer_id: erpCustomerId || '',
erp_contact_id: erpContactId || '',
sync_status: erpCustomerId ? 'success' : 'failed',
error_message: syncError || '',
retry_count: 0,
registration_data: JSON.stringify(payload)
});Why It Is Vulnerable
- •Every registration permanently stores PII (email, phone, name, company) in separate log
- •No data retention policy — SyncLog records persist indefinitely
- •RLS is admin-only — any admin can read every customer's registration data
- •registration_data is redundant for success records — data already on User profile
- •Only needed for pending/failed retries, not success cases
- •Violates GDPR data minimization principle
Exploitation Scenario
Malicious admin (or compromised admin account) queries SyncLog and extracts registration_data. They now have dump of every customer's name, email, phone, business type in JSON format — useful for phishing, competitor intelligence, or data resale.
Recommended Fix
Only store registration_data for non-success: registration_data: syncStatus !== 'success' ? JSON.stringify(payload) : null. Clear PII on success: registration_data: null. Implement retention policy: auto-delete SyncLog after 30 days.
Weak Input Validation — Multiple Distinct Weak Points
Active vulnerability — no format validation on email, phone, company name
Affected Files
const validate = () => {
if (password.length < 8) return 'Password must be at least 8 characters.';
if (password !== confirmPassword) return 'Passwords do not match.';
if (customerType === 'Company') {
if (!companyName.trim()) return 'Company Name is required.';
if (!contactPerson.trim()) return 'Contact Person is required.';
if (!businessType) return 'Business Type is required.';
} else {
if (!fullName.trim()) return 'Full Name is required.';
}
if (!phone.trim()) return 'Mobile Number is required.'; // only checks non-empty
return null;
};const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
await base44.entities.ContactInquiry.create({ ...form, status: 'new' });
setLoading(false);
setSubmitted(true);
};payload = await req.json();
const { customerType, companyName, contactPerson, email, phone, businessType } = payload;
if (!companyName || !email) {
return Response.json({ error: 'companyName and email are required' }, { status: 400 });
}Why It Is Vulnerable
Exploitation Scenario
User submits registration with phone: "'; SELECT * FROM users; --" or companyName: "<script>alert(1)</script>". Both pass !phone.trim() check. Data is stored in database, written to SyncLog, sent to ERPNext as-is.
Recommended Fix
Add backend validation: const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw 'Invalid email'; } if (companyName.length > 150) { throw 'Company name too long'; } if (phone && !/^\+?[\d\s\-()\/\.]{7,25}$/.test(phone)) { throw 'Invalid phone'; }ERP API Response Validation — Silent Failures
Active vulnerability — error responses silently skipped, code continues as if success
Affected Files
const emailRes = await fetch(`${erpUrl}/api/resource/Customer?${emailParams}`, {
headers: { Authorization: authHeader },
});
if (emailRes.ok) {
const d = await emailRes.json();
if (d.data?.length > 0) erpCustomerId = d.data[0].name;
}
// if !emailRes.ok, the block is silently skipped
// erpCustomerId stays null, code proceeds as if no duplicate existsconst emailRes = await fetch(`${erpUrl}/api/resource/Customer?${emailParams}`, {
headers: { Authorization: authHeader },
});
if (emailRes.ok) {
const d = await emailRes.json();
if (d.data?.length > 0) erpCustomerId = d.data[0].name;
}
// same silent failure patternWhy It Is Vulnerable
- •if (emailRes.ok) only checks 200-299 status codes
- •ERP timeout (5xx), auth failure (401), or rate limit (429) all return false
- •No explicit error thrown — code silently continues
- •No structured response validation for success cases
- •Raw ERP error responses (containing potentially sensitive data) stored verbatim in SyncLog
Exploitation Scenario
ERPNext is under maintenance (503). All duplicate checks silently fail. Every new registration creates duplicate Customer record. Over time, ERP system is polluted with thousands of duplicates, degrading business data integrity and causing billing/invoice routing errors.
Recommended Fix
Replace silent failure: if (!emailRes.ok) { throw new Error(`ERP duplicate check failed: HTTP ${emailRes.status}`); } const emailData = await emailRes.json(); if (!emailData.data || !Array.isArray(emailData.data)) { throw new Error('ERP returned unexpected response'); } if (emailData.data.length > 0) erpCustomerId = emailData.data[0].name;