- Location
- scripts/customer-data-collector.js:94
- Finding
- Access tokens and sensitive CRM exports are stored in plaintext without explicit restrictive permissions<![CDATA[
## Vulnerability Details
**File Location**: `scripts/customer-data-collector.js:94-96`, `scripts/customer-data-collector.js:278-294`, `scripts/tag-sync.js:96-102`, `scripts/tag-sync.js:434-454`, `scripts/scoring-engine.js:315-321`, and `scripts/strategy-output.js:305-308`
**Vulnerability Type**: Insecure local storage of credentials and sensitive business data
**Risk Level**: Medium
### Vulnerable Code
`scripts/customer-data-collector.js:94-96`:
```js
const tokenData = await resp.json();
tokenData.expires_at = Date.now() / 1000 + (tokenData.expires_in || 7200);
fs.writeFileSync(TOKEN_CACHE_PATH, JSON.stringify(tokenData, null, 2));
```
`scripts/customer-data-collector.js:278-294`:
```js
if (!dryRun) {
fs.mkdirSync(DATA_DIR, { recursive: true });
const outputPath = path.join(DATA_DIR, 'customers-raw.json');
fs.writeFileSync(outputPath, JSON.stringify(rawData, null, 2));
console.log(`[collector] 数据已写入: ${outputPath}`);
// 更新 last-sync
const syncInfo = {
last_sync_at: new Date().toISOString(),
companies_count: companies.length,
orders_count: orders.length,
trails_count: trails.length,
api_calls: apiCallCount,
duration_ms: Date.now() - startTime
};
fs.writeFileSync(path.join(DATA_DIR, 'last-sync.json'), JSON.stringify(syncInfo, null, 2));
console.log(`[collector] 同步信息已更新: last-sync.json`);
}
```
`scripts/tag-sync.js:96-102`:
```js
const tokenData = {
access_token: data.data?.access_token || data.access_token,
expires_at: Date.now() / 1000 + (data.data?.expires_in || data.expires_in || 28800)
};
fs.mkdirSync(path.dirname(TOKEN_CACHE_PATH), { recursive: true });
fs.writeFileSync(TOKEN_CACHE_PATH, JSON.stringify(tokenData, null, 2));
```
`scripts/tag-sync.js:434-454`:
```js
const dateStr = new Date().toISOString().slice(0, 10);
const backupPath = path.join(DATA_DIR, `tag-backup-${dateStr}.json`);
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.writeFileSync(backupPath, JSON.stringify(backupData, null, 2
...[truncated 3431 chars]
- Remediation
- <![CDATA[
## Remediation Suggestions
1. Apply restrictive permissions explicitly:
- Create credential and data directories with mode `0700`.
- Create token caches and CRM data files with mode `0600`.
- Verify permissions on pre-existing files and directories before reuse.
- Set an appropriately restrictive process umask at deployment time.
2. Write sensitive files safely:
- Write to a securely created temporary file in the same protected directory.
- Set mode `0600`, flush as appropriate, and atomically rename it.
- Avoid symlink-following and reject unexpected non-regular-file targets.
3. Minimize token retention:
- Store only the access token and expiration time when caching is essential.
- Do not cache the complete OAuth response.
- Prefer an operating-system credential store or managed secret service.
- Delete expired token caches and rotate exposed tokens.
4. Minimize CRM data:
- Persist only fields required for scoring and synchronization.
- Remove unused personal, contact, and free-text fields before serialization.
- Consider encryption at rest for raw exports and backups.
5. Establish retention controls:
- Define expiration periods for raw exports, previous scores, tag backups, logs, and reports.
- Automatically purge obsolete files.
- Exclude sensitive files from source control, package publication, generic CI artifacts, and broad backups.
6. Add startup checks that fail closed when sensitive directories or files are owned by an unexpected user or are accessible to group/other principals.
]]>