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
935 lines
30 KiB
Markdown
935 lines
30 KiB
Markdown
# 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:
|
|
|
|
1. **`/home/setup/navidocs/server/middleware/auth.js`** (Older, simpler)
|
|
- Basic JWT verification
|
|
- Exports: `authenticateToken`, `optionalAuth`
|
|
- No organization-level checks
|
|
|
|
2. **`/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**:
|
|
|
|
```javascript
|
|
// 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:
|
|
```javascript
|
|
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
|
|
```javascript
|
|
// 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
|
|
1. Add authentication middleware: `authenticateToken`
|
|
2. Add organization membership check: `requireOrganizationMember`
|
|
3. Remove auto-organization creation
|
|
4. Validate user belongs to specified organizationId
|
|
5. 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
|
|
```javascript
|
|
// 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
|
|
```javascript
|
|
// 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
|
|
1. Add authentication middleware
|
|
2. 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
|
|
```javascript
|
|
// 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
|
|
```javascript
|
|
// 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
|
|
```javascript
|
|
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
|
|
```javascript
|
|
// 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`:**
|
|
```javascript
|
|
// 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
|
|
```javascript
|
|
// 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
|
|
```javascript
|
|
// 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
|
|
```javascript
|
|
// 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
|
|
```javascript
|
|
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
|
|
|
|
```sql
|
|
-- 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_id` is 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)
|
|
|
|
1. **No Authentication Enforcement**
|
|
- **Impact:** Anyone can access all endpoints without authentication
|
|
- **Location:** All routes use fallback `test-user-id`
|
|
- **Fix:** Add `authenticateToken` middleware to all routes
|
|
|
|
2. **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
|
|
|
|
3. **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
|
|
|
|
4. **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
|
|
|
|
5. **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)
|
|
|
|
6. **No Audit Logging**
|
|
- **Impact:** Cannot track unauthorized access attempts
|
|
- **Fix:** Add audit logging to sensitive operations
|
|
|
|
7. **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)
|
|
|
|
8. **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`
|
|
|
|
```javascript
|
|
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`
|
|
|
|
```javascript
|
|
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:
|
|
```javascript
|
|
const userId = req.user?.id || 'test-user-id';
|
|
```
|
|
|
|
Replace with:
|
|
```javascript
|
|
const userId = req.user.id; // Will exist after authenticateToken middleware
|
|
```
|
|
|
|
---
|
|
|
|
## Positive Findings
|
|
|
|
### ✅ Well-Implemented Security Features
|
|
|
|
1. **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
|
|
|
|
2. **Search Token Generation**
|
|
- Proper tenant token generation with organization scoping
|
|
- Configurable expiration with sensible max (24h)
|
|
- Fallback mechanism for compatibility
|
|
|
|
3. **Access Control Helper (images.js)**
|
|
- Reusable verifyDocumentAccess function
|
|
- Checks organization membership, ownership, and shares
|
|
- Consistent across all image endpoints
|
|
|
|
4. **Path Traversal Protection (images.js)**
|
|
- Validates file paths stay within upload directory
|
|
- Uses path normalization for security
|
|
- Logs security violations
|
|
|
|
5. **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
|
|
|
|
6. **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:
|
|
|
|
1. **No authentication enforcement** on any routes
|
|
2. **DELETE endpoint completely unprotected**
|
|
3. **STATS endpoint exposes all tenant data**
|
|
4. **Upload endpoint accepts arbitrary organizationIds**
|
|
|
|
**Immediate Action Required:**
|
|
1. Deploy authentication middleware on all routes
|
|
2. Fix DELETE endpoint access control
|
|
3. Fix STATS endpoint organization filtering
|
|
4. Fix upload organizationId validation
|
|
5. 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`
|