- Location
- scripts/gmail-wiki-ingest.js:188
- Finding
- Email headers and thread identifiers are persisted without explicit restrictive filesystem permissions<![CDATA[
## Vulnerability Details
**File Location**: `scripts/gmail-wiki-ingest.js:188-193, 216-235, 598-608`
**Vulnerability Type**: Insecure local storage of sensitive email metadata
**Risk Level**: Medium
### Vulnerable Code
```js
function writeState(state, deps = {}) {
const file = deps.statePath || STATE_PATH;
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
fs.renameSync(tmp, file);
}
```
The persisted state includes private mail metadata:
```js
async function doFetch(opts = {}, deps = {}) {
const limit = Number(opts.limit) || 25;
const out = await postJson('/api/skill/candidates/fetch', { skill: SKILL, limit }, deps);
if (!failed(out) && out.status === 'ok') {
const items = Array.isArray(out.items) ? out.items : [];
saveState({
started_at: nowIso(deps),
n_items: items.length,
filtered: (out.filtered && typeof out.filtered === 'object') ? out.filtered : {},
items: items.map((it) => ({
thread_id: it && it.thread_id,
subject: it && it.subject,
from: it && it.from,
})),
}, deps);
}
return out;
}
```
State is deleted only after a successful report push:
```js
const content = renderReport(state, input);
const pushed = await postJson('/api/agent/push', { skill: SKILL, content }, deps);
if (failed(pushed)) return pushed;
deleteState(deps);
return { status: 'ok', content };
}
```
### Technical Analysis
The Skill deliberately avoids persisting full message bodies, which substantially reduces exposure. However, it writes Gmail thread identifiers, subjects, and sender fields to `data/last-run.json` through a temporary sibling file without explicitly setting restrictive directory or file modes.
Actual accessibility therefore depends on the process umask, parent-directory permissions, runtime user isolation, and container configuration. Under a permissive umask, the state file
...[truncated 1717 chars]
- Remediation
- <![CDATA[
## Remediation Suggestions
1. Create the state directory with mode `0700`:
```js
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
```
2. Write the temporary state file with mode `0600` and an exclusive or safely replacing creation strategy:
```js
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
```
3. Explicitly verify and correct existing directory and file permissions because `mode` does not tighten permissions on an already-existing directory.
4. Verify that the state file is owned by the dedicated runtime account and that the containing volume is not shared with unrelated workloads.
5. Delete stale state when `doReport` detects that it is older than the accepted six-hour window, where doing so does not interfere with operational recovery.
6. Add startup cleanup for abandoned temporary files and expired `last-run.json` files.
7. Consider storing opaque local item indexes instead of sender and subject fields if the server can safely provide report facts again.
8. Add tests that assert restrictive permissions and stale-state deletion on supported POSIX platforms.
]]>