T09 · Insecure Skill Coding Practices
Error
- Location
- jackal-memory/client.py:42
- Finding
- Encryption Key Stored Without Explicitly Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `jackal-memory/client.py:42-53` **Vulnerability Type**: Insecure local secret storage **Risk Level**: High ### Vulnerable Code ```python if _KEY_FILE.exists(): return bytes.fromhex(_KEY_FILE.read_text().strip()) key_hex = os.urandom(32).hex() _KEY_FILE.parent.mkdir(parents=True, exist_ok=True) _KEY_FILE.write_text(key_hex) print( "\n[jackal-memory] Generated a new encryption key and saved it to:\n" f" {_KEY_FILE}\n\n" "Your memories are encrypted with this key. Back it up:\n" f" export JACKAL_MEMORY_ENCRYPTION_KEY={key_hex}\n", file=sys.stderr, ) ``` ### Technical Analysis The AES-256 encryption key is written to `~/.config/jackal-memory/key` using `Path.write_text()` without explicitly setting restrictive permissions. The resulting mode depends on the process umask. Under a common `022` umask, the key file may be created with mode `0644`, making it readable by other local users. The confidentiality guarantee of the remote memory store depends entirely on this key. Although AES-GCM protects the uploaded content from the storage provider, access to the local key allows any party possessing the ciphertext to decrypt the memory. The code also reads existing key files without verifying their ownership, type, or permissions. It therefore does not detect or repair an insecure key file created by a previous version or altered locally. ### Attack Path 1. A user invokes `save`, `load`, or `keygen` without an existing key. 2. The client generates an AES-256 key and writes it to `~/.config/jackal-memory/key`. 3. The host's umask permits group or world read access to the new file. 4. Another local account reads the key file. 5. The attacker separately obtains encrypted memory through access to the storage API, captured traffic at an authorized endpoint, backups, or another compromised component. 6. The attacker uses the stolen key to decrypt the stored memory. ### Impact Assessment A ...[truncated 415 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions - Create the key file atomically with mode `0600`, rather than relying on the process umask. - Set the parent directory to mode `0700`. - Reject symbolic links and verify that the key file is a regular file owned by the current user. - Check existing file permissions before reading the key. Refuse access or repair permissions if group or world access is present. - Avoid overwriting an existing key through a non-atomic check-then-write sequence. - Use an operating-system credential store or keyring where available. For example, create the file with exclusive and restrictive flags: ```python _KEY_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(_KEY_FILE.parent, 0o700) fd = os.open(_KEY_FILE, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) try: with os.fdopen(fd, "w") as key_file: key_file.write(key_hex) finally: os.chmod(_KEY_FILE, 0o600) ``` ]]>
