Fixed:
- Price: €800K-€1.5M, Sunseeker added
- Agent 1: Joe Trader persona + actual sale ads research
- Ignored meilisearch binary + data/ (too large for GitHub)
- SESSION_DEBUG_BLOCKERS.md created
Ready for Session 1 launch.
🤖 Generated with Claude Code
30 KiB
NaviDocs Multi-Tenancy Security Audit Report
Date: October 23, 2025 Auditor: Claude Code Scope: Document isolation and organizationId enforcement across all API endpoints
Executive Summary
This audit evaluates the multi-tenancy implementation in NaviDocs to ensure documents are properly scoped to organizations and that single-boat tenants (like Liliane1) cannot access documents from other organizations.
Overall Security Rating: 🔴 CRITICAL VULNERABILITIES FOUND
Critical Findings
- DELETE endpoint has NO access control - Any user can delete any document
- STATS endpoint exposes ALL documents - No organization filtering
- No authentication middleware enforcement - Routes use fallback test user IDs
- Missing organizationId validation in upload endpoint
Authentication Architecture
Current Implementation
NaviDocs has TWO authentication middleware files with different implementations:
-
/home/setup/navidocs/server/middleware/auth.js(Older, simpler)- Basic JWT verification
- Exports:
authenticateToken,optionalAuth - No organization-level checks
-
/home/setup/navidocs/server/middleware/auth.middleware.js(Newer, comprehensive)- Full JWT verification with audit logging
- Exports:
authenticateToken,requireEmailVerified,requireActiveAccount,requireOrganizationMember,requireOrganizationRole,requireEntityPermission,requireSystemAdmin,optionalAuth - Includes organization and entity permission checks
Current Route Protection Status
CRITICAL ISSUE: Based on /home/setup/navidocs/server/index.js, NONE of the document routes use authentication middleware:
// No authentication middleware on these routes:
app.use('/api/upload', uploadRoutes);
app.use('/api/documents', documentsRoutes);
app.use('/api/stats', statsRoutes);
app.use('/api/search', searchRoutes);
app.use('/api', imagesRoutes);
All routes fall back to:
const userId = req.user?.id || 'test-user-id';
Endpoint-by-Endpoint Analysis
1. Document Upload (POST /api/upload)
File: /home/setup/navidocs/server/routes/upload.js
Current Implementation
// Line 50: organizationId is required in request body
const { title, documentType, organizationId, entityId, componentId, subEntityId } = req.body;
// Line 60: Validates required fields
if (!title || !documentType || !organizationId) {
return res.status(400).json({
error: 'Missing required fields: title, documentType, organizationId'
});
}
// Lines 94-102: Auto-creates organization if it doesn't exist
const existingOrg = db.prepare('SELECT id FROM organizations WHERE id = ?').get(organizationId);
if (!existingOrg) {
console.log(`Creating new organization: ${organizationId}`);
db.prepare(`
INSERT INTO organizations (id, name, created_at, updated_at)
VALUES (?, ?, ?, ?)
`).run(organizationId, organizationId, Date.now(), Date.now());
}
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No authentication | 🔴 CRITICAL | Anyone can upload without authentication |
| No organization membership check | 🔴 CRITICAL | User can upload to ANY organizationId they specify |
| Auto-creates organizations | 🔴 CRITICAL | Allows creation of arbitrary organizations |
| Client controls organizationId | 🔴 CRITICAL | Should be derived from authenticated user context |
Recommended Fixes
- Add authentication middleware:
authenticateToken - Add organization membership check:
requireOrganizationMember - Remove auto-organization creation
- Validate user belongs to specified organizationId
- Consider deriving organizationId from user's active organization context
2. Document Listing (GET /api/documents)
File: /home/setup/navidocs/server/routes/documents.js
Current Implementation
// Lines 240-257: PROPER tenant isolation with INNER JOIN
let query = `
SELECT
d.id,
d.organization_id,
d.entity_id,
d.title,
d.document_type,
d.file_name,
d.file_size,
d.page_count,
d.status,
d.created_at,
d.updated_at
FROM documents d
INNER JOIN user_organizations uo ON d.organization_id = uo.organization_id
WHERE uo.user_id = ?
`;
Security Assessment
| Aspect | Status | Notes |
|---|---|---|
| organizationId filtering | ✅ GOOD | Uses INNER JOIN on user_organizations |
| Query construction | ✅ GOOD | Filters by userId from user_organizations |
| Additional filters | ✅ GOOD | Optional organizationId, entityId, documentType, status |
| SQL injection protection | ✅ GOOD | Uses parameterized queries |
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No authentication | 🟡 HIGH | Falls back to test-user-id |
| Optional organizationId filter | 🟢 LOW | User can query across all their orgs (acceptable) |
Recommendation
✅ Implementation is CORRECT - Once authentication middleware is added, this endpoint properly enforces tenant isolation.
3. Document Retrieval (GET /api/documents/:id)
File: /home/setup/navidocs/server/routes/documents.js
Current Implementation
// Lines 41-63: Gets document without initial organizationId filter
const document = db.prepare(`
SELECT
d.id,
d.organization_id,
d.entity_id,
...
FROM documents d
WHERE d.id = ?
`).get(id);
// Lines 70-79: Access check AFTER retrieving document
const hasAccess = db.prepare(`
SELECT 1 FROM user_organizations
WHERE user_id = ? AND organization_id = ?
UNION
SELECT 1 FROM documents
WHERE id = ? AND uploaded_by = ?
UNION
SELECT 1 FROM document_shares
WHERE document_id = ? AND shared_with = ?
`).get(userId, document.organization_id, id, userId, id, userId);
Security Assessment
| Aspect | Status | Notes |
|---|---|---|
| organizationId check | ✅ GOOD | Verifies user belongs to document's organization |
| Document shares support | ✅ GOOD | Allows shared document access |
| Upload ownership check | ✅ GOOD | Document owner can always access |
| Access denied response | ✅ GOOD | Returns 403 if no access |
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No authentication | 🟡 HIGH | Falls back to test-user-id |
| Information disclosure | 🟢 LOW | Returns 404 vs 403 for non-existent docs (acceptable) |
| Entity/Component queries | 🟡 MEDIUM | Lines 104-119 don't re-verify organization ownership |
Recommendations
- Add authentication middleware
- Consider verifying entity/component belong to same organization as document (defense in depth)
4. Document PDF Retrieval (GET /api/documents/:id/pdf)
File: /home/setup/navidocs/server/routes/documents.js
Current Implementation
// Lines 191-195: Gets document
const doc = db.prepare(`
SELECT id, organization_id, file_path, file_name
FROM documents
WHERE id = ?
`).get(id);
// Lines 199-203: Access check (same as retrieval endpoint)
const hasAccess = db.prepare(`
SELECT 1 FROM user_organizations WHERE user_id = ? AND organization_id = ?
UNION SELECT 1 FROM documents WHERE id = ? AND uploaded_by = ?
UNION SELECT 1 FROM document_shares WHERE document_id = ? AND shared_with = ?
`).get(userId, doc.organization_id, id, userId, id, userId);
Security Assessment
✅ GOOD - Same access control as document retrieval endpoint
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No authentication | 🟡 HIGH | Falls back to test-user-id |
5. Document Deletion (DELETE /api/documents/:id)
File: /home/setup/navidocs/server/routes/documents.js
Current Implementation
// Lines 354-414: CRITICAL VULNERABILITY
router.delete('/:id', async (req, res) => {
const { id } = req.params;
const db = getDb();
const searchClient = getMeilisearchClient();
// Get document info before deletion
const document = db.prepare('SELECT * FROM documents WHERE id = ?').get(id);
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
// ❌ NO ACCESS CONTROL CHECK ❌
// Delete from Meilisearch index
try {
const index = await searchClient.getIndex(MEILISEARCH_INDEX_NAME);
const filter = `docId = "${id}"`;
await index.deleteDocuments({ filter });
} catch (err) {
logger.warn(`Meilisearch cleanup failed for ${id}:`, err);
}
// Delete from database
const deleteStmt = db.prepare('DELETE FROM documents WHERE id = ?');
deleteStmt.run(id);
// Delete from filesystem
const docFolder = path.join(uploadsDir, id);
if (fs.existsSync(docFolder)) {
await rm(docFolder, { recursive: true, force: true });
}
res.json({
success: true,
message: 'Document deleted successfully',
documentId: id,
title: document.title
});
});
Security Assessment
| Aspect | Status | Notes |
|---|---|---|
| organizationId check | 🔴 MISSING | NO ACCESS CONTROL AT ALL |
| User authentication | 🔴 MISSING | Not even checked |
| Organization membership | 🔴 MISSING | Not verified |
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No access control | 🔴 CRITICAL | ANY USER CAN DELETE ANY DOCUMENT |
| No authentication | 🔴 CRITICAL | Endpoint is completely unprotected |
| No audit trail | 🟡 HIGH | Deletion not logged with user context |
Recommended Fix
router.delete('/:id', async (req, res) => {
const { id } = req.params;
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Authentication required' });
}
const db = getDb();
// Get document with organization
const document = db.prepare('SELECT * FROM documents WHERE id = ?').get(id);
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
// ✅ ADD ACCESS CONTROL CHECK
const hasAccess = db.prepare(`
SELECT 1 FROM user_organizations
WHERE user_id = ? AND organization_id = ?
`).get(userId, document.organization_id);
if (!hasAccess) {
logger.warn(`Unauthorized deletion attempt`, {
userId,
documentId: id,
organizationId: document.organization_id
});
return res.status(403).json({
error: 'Access denied',
message: 'You do not have permission to delete this document'
});
}
// ✅ ADD ROLE CHECK (only admins/managers should delete)
const userRole = db.prepare(`
SELECT role FROM user_organizations
WHERE user_id = ? AND organization_id = ?
`).get(userId, document.organization_id);
if (!['admin', 'manager'].includes(userRole?.role)) {
return res.status(403).json({
error: 'Insufficient permissions',
message: 'Only admins and managers can delete documents'
});
}
// Proceed with deletion...
});
6. Search Token Generation (POST /api/search/token)
File: /home/setup/navidocs/server/routes/search.js
Current Implementation
// Lines 34-40: Gets user's organizations
const orgs = db.prepare(`
SELECT organization_id
FROM user_organizations
WHERE user_id = ?
`).all(userId);
const organizationIds = orgs.map(org => org.organization_id);
if (organizationIds.length === 0) {
return res.status(403).json({ error: 'No organizations found for user' });
}
// Lines 50-52: Generates tenant token with organization filter
const token = await generateTenantToken(userId, organizationIds, tokenExpiry);
/home/setup/navidocs/server/config/meilisearch.js:
// Lines 101-106: Tenant token filter construction
export async function generateTenantToken(userId, organizationIds, expiresIn = 3600) {
const orgList = (organizationIds || []).map((id) => `"${id}"`).join(', ');
const filter = `userId = "${userId}" OR organizationId IN [${orgList}]`;
const searchRules = {
[INDEX_NAME]: { filter }
};
// ...
}
Security Assessment
| Aspect | Status | Notes |
|---|---|---|
| organizationId filtering | ✅ EXCELLENT | Generates tenant-scoped token |
| Multi-organization support | ✅ GOOD | Supports users in multiple orgs |
| Token expiration | ✅ GOOD | Configurable with 24h max |
| Filter syntax | ✅ GOOD | Properly quotes string values |
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No authentication | 🟡 HIGH | Falls back to test-user-id |
| userId filter redundancy | 🟢 LOW | userId = "${userId}" filter might be unnecessary |
Recommendation
✅ Implementation is EXCELLENT - Proper tenant token generation with organization scoping.
7. Server-Side Search (POST /api/search)
File: /home/setup/navidocs/server/routes/search.js
Current Implementation
// Lines 98-104: Gets user's organizations
const orgs = db.prepare(`
SELECT organization_id
FROM user_organizations
WHERE user_id = ?
`).all(userId);
const organizationIds = orgs.map(org => org.organization_id);
// Lines 112-114: Builds organization filter
const filterParts = [
`userId = "${userId}" OR organizationId IN [${organizationIds.map(id => `"${id}"`).join(', ')}]`
];
Security Assessment
✅ GOOD - Properly filters search results by user's organizations
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No authentication | 🟡 HIGH | Falls back to test-user-id |
8. Image Retrieval Endpoints
File: /home/setup/navidocs/server/routes/images.js
Current Implementation
// Lines 32-49: verifyDocumentAccess helper function
async function verifyDocumentAccess(documentId, userId, db) {
const document = db.prepare('SELECT id, organization_id FROM documents WHERE id = ?').get(documentId);
if (!document) {
return { hasAccess: false, error: 'Document not found', status: 404 };
}
const hasAccess = db.prepare(`
SELECT 1 FROM user_organizations WHERE user_id = ? AND organization_id = ?
UNION SELECT 1 FROM documents WHERE id = ? AND uploaded_by = ?
UNION SELECT 1 FROM document_shares WHERE document_id = ? AND shared_with = ?
`).get(userId, document.organization_id, documentId, userId, documentId, userId);
if (!hasAccess) {
return { hasAccess: false, error: 'Access denied', status: 403 };
}
return { hasAccess: true, document };
}
All image endpoints (GET /api/documents/:id/images, GET /api/documents/:id/pages/:pageNum/images, GET /api/images/:imageId) use this helper.
Security Assessment
✅ GOOD - Consistent access control through helper function
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No authentication | 🟡 HIGH | Falls back to test-user-id |
| Path traversal protection | ✅ GOOD | Lines 298-308 validate file path |
9. Statistics Endpoint (GET /api/stats)
File: /home/setup/navidocs/server/routes/stats.js
Current Implementation
// Lines 18-81: NO organizationId filtering
router.get('/', async (req, res) => {
try {
const db = getDb();
// ❌ EXPOSES ALL DOCUMENTS ❌
const { totalDocuments } = db.prepare(`
SELECT COUNT(*) as totalDocuments FROM documents
`).get();
const { totalPages } = db.prepare(`
SELECT COUNT(*) as totalPages FROM document_pages
`).get();
const documentsByStatus = db.prepare(`
SELECT status, COUNT(*) as count
FROM documents
GROUP BY status
`).all();
// Shows recent documents from ALL organizations
const recentDocuments = db.prepare(`
SELECT id, title, status, created_at, page_count
FROM documents
ORDER BY created_at DESC
LIMIT 5
`).all();
// ...
}
});
Security Assessment
| Aspect | Status | Notes |
|---|---|---|
| organizationId filtering | 🔴 MISSING | Shows stats across ALL organizations |
| User authentication | 🔴 MISSING | Not checked |
| Data exposure | 🔴 CRITICAL | Leaks document counts, titles, status |
Security Issues
| Issue | Severity | Description |
|---|---|---|
| No organization filtering | 🔴 CRITICAL | EXPOSES DATA FROM ALL TENANTS |
| No authentication | 🔴 CRITICAL | Anyone can view system-wide stats |
| Information disclosure | 🔴 CRITICAL | Reveals document titles, counts, storage usage |
Recommended Fix
router.get('/', async (req, res) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Authentication required' });
}
const db = getDb();
// ✅ Get user's organizations
const orgs = db.prepare(`
SELECT organization_id FROM user_organizations WHERE user_id = ?
`).all(userId);
const orgIds = orgs.map(o => o.organization_id);
if (orgIds.length === 0) {
return res.status(403).json({ error: 'No organizations found' });
}
// ✅ Filter by organization
const placeholders = orgIds.map(() => '?').join(',');
const { totalDocuments } = db.prepare(`
SELECT COUNT(*) as totalDocuments
FROM documents
WHERE organization_id IN (${placeholders})
`).get(...orgIds);
// ... apply same filtering to all queries
}
});
Database Schema Analysis
File: /home/setup/navidocs/server/db/schema.sql
Multi-Tenancy Design
-- Organizations table (Line 22-28)
CREATE TABLE organizations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT DEFAULT 'personal',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- User-Organization membership (Line 31-39)
CREATE TABLE user_organizations (
user_id TEXT NOT NULL,
organization_id TEXT NOT NULL,
role TEXT DEFAULT 'member', -- admin, manager, member, viewer
joined_at INTEGER NOT NULL,
PRIMARY KEY (user_id, organization_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
);
-- Documents table (Line 112-149)
CREATE TABLE documents (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL, -- ✅ REQUIRED organizationId
entity_id TEXT,
sub_entity_id TEXT,
component_id TEXT,
uploaded_by TEXT NOT NULL,
-- ...
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
-- ...
);
-- Indexes (Line 253-261)
CREATE INDEX idx_documents_org ON documents(organization_id); -- ✅ Indexed
Assessment
✅ Schema is well-designed for multi-tenancy:
organization_idis NOT NULL and indexed- Foreign key cascade on organization deletion
- User-organization many-to-many relationship with roles
- Document shares table for cross-user document access
Test Scenarios for Tenant Isolation
Scenario 1: Cross-Organization Document Access
Setup:
- User A belongs to Organization "Liliane1"
- User B belongs to Organization "Yacht-Club-Marina"
- Document D1 uploaded to "Liliane1"
Test Cases:
| Test | Expected Result | Current Result |
|---|---|---|
| User B calls GET /api/documents/:D1 | 403 Forbidden | ✅ 403 (if authenticated) |
| User B calls GET /api/documents with no filter | Empty list | ✅ Empty (if authenticated) |
| User B calls GET /api/documents/:D1/pdf | 403 Forbidden | ✅ 403 (if authenticated) |
| User B calls DELETE /api/documents/:D1 | 403 Forbidden | 🔴 200 Success (VULNERABLE) |
| User B searches for D1 content | No results | ✅ No results (if authenticated) |
Scenario 2: Malicious Upload to Another Organization
Setup:
- User A belongs to "Liliane1"
- User A attempts upload with organizationId="Yacht-Club-Marina"
Test Cases:
| Test | Expected Result | Current Result |
|---|---|---|
| Upload with wrong organizationId | 403 Forbidden | 🔴 200 Success (VULNERABLE) |
| Upload creates new organization | Should fail | 🔴 Creates org (VULNERABLE) |
Scenario 3: Multi-Organization User
Setup:
- User C belongs to both "Liliane1" and "Yacht-Club-Marina"
- Documents exist in both organizations
Test Cases:
| Test | Expected Result | Current Result |
|---|---|---|
| GET /api/documents (no filter) | All docs from both orgs | ✅ Correct (if authenticated) |
| GET /api/documents?organizationId=Liliane1 | Only Liliane1 docs | ✅ Correct (if authenticated) |
| Search returns results | From both organizations | ✅ Correct (if authenticated) |
| Stats endpoint | Combined stats for both orgs | 🔴 Shows ALL orgs (VULNERABLE) |
Scenario 4: Document Sharing
Setup:
- User A uploads document D1 to "Liliane1"
- User A shares D1 with User B (different organization)
Test Cases:
| Test | Expected Result | Current Result |
|---|---|---|
| User B accesses shared document | 200 Success | ✅ Correct (if authenticated) |
| User B appears in sharing check | Found via document_shares | ✅ Correct (if authenticated) |
| User B tries to delete shared doc | 403 Forbidden | 🔴 200 Success (VULNERABLE) |
Critical Vulnerabilities Summary
🔴 Critical (Must Fix Immediately)
-
No Authentication Enforcement
- Impact: Anyone can access all endpoints without authentication
- Location: All routes use fallback
test-user-id - Fix: Add
authenticateTokenmiddleware to all routes
-
DELETE Endpoint Unprotected
- Impact: Any user can delete any document
- Location:
/home/setup/navidocs/server/routes/documents.js:354-414 - Fix: Add organization membership and role checks
-
STATS Endpoint Exposes All Data
- Impact: Leaks information across all tenants
- Location:
/home/setup/navidocs/server/routes/stats.js:18-131 - Fix: Filter by user's organizations
-
Upload Accepts Arbitrary organizationId
- Impact: Users can upload to any organization
- Location:
/home/setup/navidocs/server/routes/upload.js:50-64 - Fix: Validate user belongs to specified organization
-
Upload Auto-Creates Organizations
- Impact: Users can create arbitrary organizations
- Location:
/home/setup/navidocs/server/routes/upload.js:94-102 - Fix: Remove auto-creation, require pre-existing organizations
🟡 High (Should Fix Soon)
-
No Audit Logging
- Impact: Cannot track unauthorized access attempts
- Fix: Add audit logging to sensitive operations
-
Missing Rate Limiting on Image Endpoints
- Impact: Potential DoS via image requests
- Location: Image endpoints have rate limiting but should be stricter
- Fix: Review and tighten rate limits
🟢 Low (Recommended)
- Entity/Component Organization Consistency
- Impact: Could reference entities from other organizations
- Location: Document retrieval endpoints
- Fix: Add validation that entity/component belongs to same org as document
Recommended Security Fixes
Priority 1: Add Authentication Middleware
File: /home/setup/navidocs/server/index.js
import { authenticateToken } from './middleware/auth.middleware.js';
// Protect all document-related routes
app.use('/api/upload', authenticateToken, uploadRoutes);
app.use('/api/documents', authenticateToken, documentsRoutes);
app.use('/api/stats', authenticateToken, statsRoutes);
app.use('/api/search', authenticateToken, searchRoutes);
app.use('/api', authenticateToken, imagesRoutes);
app.use('/api', authenticateToken, tocRoutes);
app.use('/api/jobs', authenticateToken, jobsRoutes);
Priority 2: Fix DELETE Endpoint
File: /home/setup/navidocs/server/routes/documents.js
Add the access control check from the "Recommended Fix" section above (lines 354-414).
Priority 3: Fix STATS Endpoint
File: /home/setup/navidocs/server/routes/stats.js
Add organization filtering to all queries as shown in the "Recommended Fix" section.
Priority 4: Fix Upload Validation
File: /home/setup/navidocs/server/routes/upload.js
router.post('/', authenticateToken, upload.single('file'), async (req, res) => {
const { organizationId } = req.body;
const userId = req.user.id; // No fallback after authentication middleware
// ✅ Validate user belongs to organization
const membership = db.prepare(`
SELECT role FROM user_organizations
WHERE user_id = ? AND organization_id = ?
`).get(userId, organizationId);
if (!membership) {
return res.status(403).json({
error: 'Access denied',
message: 'You are not a member of this organization'
});
}
// ✅ Remove auto-creation logic (lines 94-102)
const existingOrg = db.prepare('SELECT id FROM organizations WHERE id = ?').get(organizationId);
if (!existingOrg) {
return res.status(400).json({
error: 'Invalid organization',
message: 'Organization does not exist'
});
}
// Continue with upload...
});
Priority 5: Add Audit Logging
Create audit log entries for:
- Document deletions
- Failed access attempts
- Organization membership changes
- Document sharing
Priority 6: Remove Fallback Test User IDs
Search codebase and remove all instances of:
const userId = req.user?.id || 'test-user-id';
Replace with:
const userId = req.user.id; // Will exist after authenticateToken middleware
Positive Findings
✅ Well-Implemented Security Features
-
Document List Query (GET /api/documents)
- Excellent use of INNER JOIN on user_organizations
- Properly scoped to user's organizations
- Parameterized queries prevent SQL injection
-
Search Token Generation
- Proper tenant token generation with organization scoping
- Configurable expiration with sensible max (24h)
- Fallback mechanism for compatibility
-
Access Control Helper (images.js)
- Reusable verifyDocumentAccess function
- Checks organization membership, ownership, and shares
- Consistent across all image endpoints
-
Path Traversal Protection (images.js)
- Validates file paths stay within upload directory
- Uses path normalization for security
- Logs security violations
-
Database Schema Design
- organization_id is NOT NULL and properly indexed
- Foreign key cascades for data integrity
- User-organization many-to-many with roles
- Document shares table for granular access
-
SQL Injection Protection
- All queries use parameterized statements
- No string concatenation in SQL queries
Testing Checklist
Use this checklist to verify tenant isolation after fixes are applied:
Authentication Tests
- Verify all endpoints reject requests without valid JWT
- Verify expired tokens are rejected
- Verify invalid tokens are rejected
- Verify test-user-id fallback is removed
Document Upload Tests
- User can upload to their own organization
- User CANNOT upload to organization they don't belong to
- Upload with non-existent organizationId returns 400
- Upload does NOT auto-create organizations
Document Access Tests
- User can list only documents from their organizations
- User CANNOT access documents from other organizations
- Multi-org user sees documents from all their organizations
- User can access shared documents
- User CANNOT access documents not shared with them
Document Deletion Tests
- Admin can delete documents in their organization
- Manager can delete documents in their organization
- Member CANNOT delete documents
- User CANNOT delete documents from other organizations
- Deletion logs audit event with user context
Search Tests
- Search results only include documents from user's organizations
- Tenant token filters correctly by organizationId
- Multi-org user gets results from all their organizations
- User CANNOT search documents from other organizations
Stats Tests
- Stats show only data from user's organizations
- Multi-org user sees combined stats
- System admin sees system-wide stats (if applicable)
Image Access Tests
- User can access images from their documents
- User CANNOT access images from other organization's documents
- Path traversal attempts are blocked and logged
Conclusion
NaviDocs has a well-designed multi-tenancy database schema and several properly implemented endpoints (document listing, search token generation, image access control). However, critical vulnerabilities exist that allow unauthorized access and data manipulation:
- No authentication enforcement on any routes
- DELETE endpoint completely unprotected
- STATS endpoint exposes all tenant data
- Upload endpoint accepts arbitrary organizationIds
Immediate Action Required:
- Deploy authentication middleware on all routes
- Fix DELETE endpoint access control
- Fix STATS endpoint organization filtering
- Fix upload organizationId validation
- Remove test-user-id fallbacks
Once these fixes are implemented, NaviDocs will have strong multi-tenancy isolation that ensures single-boat tenants like Liliane1 can only access their own documents.
References
- Database Schema:
/home/setup/navidocs/server/db/schema.sql - Documents Routes:
/home/setup/navidocs/server/routes/documents.js - Upload Routes:
/home/setup/navidocs/server/routes/upload.js - Search Routes:
/home/setup/navidocs/server/routes/search.js - Images Routes:
/home/setup/navidocs/server/routes/images.js - Stats Routes:
/home/setup/navidocs/server/routes/stats.js - Auth Middleware:
/home/setup/navidocs/server/middleware/auth.middleware.js - Meilisearch Config:
/home/setup/navidocs/server/config/meilisearch.js - Server Entry:
/home/setup/navidocs/server/index.js