Compare commits
1 Commits
fix/1430
...
burn/1423-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e98ce44ee1 |
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Commit-msg hook: warn about shell injection risks
|
||||
# Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
|
||||
|
||||
COMMIT_MSG_FILE="$1"
|
||||
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
||||
|
||||
# Check for dangerous patterns
|
||||
DANGEROUS_PATTERNS=(
|
||||
'`' # Backticks
|
||||
'$(' # Command substitution
|
||||
'${' # Variable expansion
|
||||
'\\`' # Escaped backticks
|
||||
'eval ' # eval command
|
||||
'exec ' # exec command
|
||||
'source ' # source command
|
||||
'|' # Pipe
|
||||
'&&' # AND operator
|
||||
'||' # OR operator
|
||||
';' # Semicolon
|
||||
'>' # Redirect
|
||||
'<' # Input redirect
|
||||
)
|
||||
|
||||
FOUND_ISSUES=()
|
||||
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
||||
if echo "$COMMIT_MSG" | grep -q "$pattern"; then
|
||||
FOUND_ISSUES+=("$pattern")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then
|
||||
echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:"
|
||||
for issue in "${FOUND_ISSUES[@]}"; do
|
||||
echo " - $issue"
|
||||
done
|
||||
echo ""
|
||||
echo "This could trigger shell execution during git operations."
|
||||
echo ""
|
||||
echo "Safe alternatives:"
|
||||
echo " 1. Use: git commit -F <file> instead of git commit -m"
|
||||
echo " 2. Escape special characters in commit messages"
|
||||
echo " 3. Use the safe_commit() function from bin/safe_commit.py"
|
||||
echo ""
|
||||
echo "To proceed anyway, use: git commit --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
6
app.js
6
app.js
@@ -2140,9 +2140,9 @@ function setupControls() {
|
||||
}
|
||||
|
||||
function sendChatMessage(overrideText = null) {
|
||||
// Mine chat message to MemPalace
|
||||
if (overrideText) {
|
||||
window.electronAPI.execPython(`mempalace add_drawer "${this.wing}" "chat" "${overrideText}"`);
|
||||
// Mine chat message to MemPalace — use safe addDrawer method
|
||||
if (overrideText && typeof mempalace !== 'undefined' && mempalace.addDrawer) {
|
||||
mempalace.addDrawer(this.wing, 'chat', overrideText);
|
||||
}
|
||||
const input = document.getElementById('chat-input');
|
||||
const text = overrideText || input.value.trim();
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Safe commit message handler to prevent shell injection.
|
||||
|
||||
Issue #1430: [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message
|
||||
|
||||
This script provides safe ways to commit with code-containing messages.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def escape_shell_chars(text: str) -> str:
|
||||
"""
|
||||
Escape shell-sensitive characters in text.
|
||||
|
||||
This prevents shell injection when text is used in shell commands.
|
||||
"""
|
||||
# Characters that need escaping in shell
|
||||
shell_chars = ['$', '`', '\\', '"', "'", '!', '(', ')', '{', '}', '[', ']',
|
||||
'|', '&', ';', '<', '>', '*', '?', '~', '#']
|
||||
|
||||
escaped = text
|
||||
for char in shell_chars:
|
||||
escaped = escaped.replace(char, '\\' + char)
|
||||
|
||||
return escaped
|
||||
|
||||
|
||||
def safe_commit_message(message: str) -> str:
|
||||
"""
|
||||
Create a safe commit message by escaping shell-sensitive characters.
|
||||
|
||||
Args:
|
||||
message: The commit message
|
||||
|
||||
Returns:
|
||||
Escaped commit message safe for shell use
|
||||
"""
|
||||
return escape_shell_chars(message)
|
||||
|
||||
|
||||
def commit_with_file(message: str, branch: str = None) -> bool:
|
||||
"""
|
||||
Commit using a temporary file instead of -m flag.
|
||||
|
||||
This is the safest way to commit messages containing code or special characters.
|
||||
|
||||
Args:
|
||||
message: The commit message
|
||||
branch: Optional branch name
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Create temporary file for commit message
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||
f.write(message)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Build git command
|
||||
cmd = ['git', 'commit', '-F', temp_file]
|
||||
if branch:
|
||||
cmd.extend(['-b', branch])
|
||||
|
||||
# Execute git commit
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✅ Committed successfully using file: {temp_file}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Commit failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def commit_safe(message: str, use_file: bool = True) -> bool:
|
||||
"""
|
||||
Safely commit with a message.
|
||||
|
||||
Args:
|
||||
message: The commit message
|
||||
use_file: If True, use -F <file> instead of -m
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if use_file:
|
||||
return commit_with_file(message)
|
||||
else:
|
||||
# Use escaped message with -m flag
|
||||
escaped_message = safe_commit_message(message)
|
||||
cmd = ['git', 'commit', '-m', escaped_message]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("✅ Committed successfully with escaped message")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Commit failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
|
||||
def check_commit_message_safety(message: str) -> dict:
|
||||
"""
|
||||
Check if a commit message contains potentially dangerous patterns.
|
||||
|
||||
Args:
|
||||
message: The commit message to check
|
||||
|
||||
Returns:
|
||||
Dictionary with safety analysis
|
||||
"""
|
||||
dangerous_patterns = [
|
||||
(r'`[^`]*`', 'Backticks (shell command substitution)'),
|
||||
(r'\$\([^)]*\)', 'Command substitution $(...)'),
|
||||
(r'\$\{[^}]*\}', 'Variable expansion ${...}'),
|
||||
(r'\\`', 'Escaped backticks'),
|
||||
(r'eval\s+', 'eval command'),
|
||||
(r'exec\s+', 'exec command'),
|
||||
(r'source\s+', 'source command'),
|
||||
(r'\.\s+', 'dot command'),
|
||||
(r'\|\s*', 'Pipe character'),
|
||||
(r'&&', 'AND operator'),
|
||||
(r'\|\|', 'OR operator'),
|
||||
(r';', 'Semicolon (command separator)'),
|
||||
(r'>', 'Redirect operator'),
|
||||
(r'<', 'Input redirect'),
|
||||
]
|
||||
|
||||
findings = []
|
||||
for pattern, description in dangerous_patterns:
|
||||
matches = re.findall(pattern, message)
|
||||
if matches:
|
||||
findings.append({
|
||||
'pattern': pattern,
|
||||
'description': description,
|
||||
'matches': matches,
|
||||
'count': len(matches)
|
||||
})
|
||||
|
||||
return {
|
||||
'safe': len(findings) == 0,
|
||||
'findings': findings,
|
||||
'recommendation': 'Use commit_with_file() or escape_shell_chars()' if findings else 'Message appears safe'
|
||||
}
|
||||
|
||||
|
||||
def create_commit_hook_guard():
|
||||
"""
|
||||
Create a commit-msg hook that warns about dangerous patterns.
|
||||
"""
|
||||
hook_content = '''#!/usr/bin/env bash
|
||||
# Commit-msg hook: warn about shell injection risks
|
||||
# Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
|
||||
|
||||
COMMIT_MSG_FILE="$1"
|
||||
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
||||
|
||||
# Check for dangerous patterns
|
||||
DANGEROUS_PATTERNS=(
|
||||
'`' # Backticks
|
||||
'$(' # Command substitution
|
||||
'${' # Variable expansion
|
||||
'\\`' # Escaped backticks
|
||||
'eval ' # eval command
|
||||
'exec ' # exec command
|
||||
'source ' # source command
|
||||
'|' # Pipe
|
||||
'&&' # AND operator
|
||||
'||' # OR operator
|
||||
';' # Semicolon
|
||||
'>' # Redirect
|
||||
'<' # Input redirect
|
||||
)
|
||||
|
||||
FOUND_ISSUES=()
|
||||
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
||||
if echo "$COMMIT_MSG" | grep -q "$pattern"; then
|
||||
FOUND_ISSUES+=("$pattern")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then
|
||||
echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:"
|
||||
for issue in "${FOUND_ISSUES[@]}"; do
|
||||
echo " - $issue"
|
||||
done
|
||||
echo ""
|
||||
echo "This could trigger shell execution during git operations."
|
||||
echo ""
|
||||
echo "Safe alternatives:"
|
||||
echo " 1. Use: git commit -F <file> instead of git commit -m"
|
||||
echo " 2. Escape special characters in commit messages"
|
||||
echo " 3. Use the safe_commit() function from bin/safe_commit.py"
|
||||
echo ""
|
||||
echo "To proceed anyway, use: git commit --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
'''
|
||||
|
||||
return hook_content
|
||||
|
||||
|
||||
def install_commit_hook():
|
||||
"""
|
||||
Install the commit-msg hook to warn about dangerous patterns.
|
||||
"""
|
||||
hook_path = Path('.git/hooks/commit-msg')
|
||||
hook_content = create_commit_hook_guard()
|
||||
|
||||
# Check if .git/hooks exists
|
||||
if not hook_path.parent.exists():
|
||||
print("❌ .git/hooks directory not found")
|
||||
return False
|
||||
|
||||
# Write hook
|
||||
with open(hook_path, 'w') as f:
|
||||
f.write(hook_content)
|
||||
|
||||
# Make executable
|
||||
os.chmod(hook_path, 0o755)
|
||||
|
||||
print(f"✅ Installed commit-msg hook to {hook_path}")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for safe commit tool."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Safe commit message handling")
|
||||
parser.add_argument("--message", "-m", help="Commit message")
|
||||
parser.add_argument("--file", "-F", help="Read commit message from file")
|
||||
parser.add_argument("--check", action="store_true", help="Check message safety")
|
||||
parser.add_argument("--install-hook", action="store_true", help="Install commit-msg hook")
|
||||
parser.add_argument("--escape", action="store_true", help="Escape shell characters in message")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.install_hook:
|
||||
if install_commit_hook():
|
||||
print("Commit hook installed successfully")
|
||||
else:
|
||||
print("Failed to install commit hook")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
if args.check:
|
||||
if args.message:
|
||||
safety = check_commit_message_safety(args.message)
|
||||
print(f"Message safety check:")
|
||||
print(f" Safe: {safety['safe']}")
|
||||
print(f" Recommendation: {safety['recommendation']}")
|
||||
if safety['findings']:
|
||||
print(f" Findings:")
|
||||
for finding in safety['findings']:
|
||||
print(f" - {finding['description']}: {finding['count']} matches")
|
||||
else:
|
||||
print("Please provide a message with --message")
|
||||
return
|
||||
|
||||
if args.escape:
|
||||
if args.message:
|
||||
escaped = safe_commit_message(args.message)
|
||||
print(f"Escaped message:")
|
||||
print(escaped)
|
||||
else:
|
||||
print("Please provide a message with --message")
|
||||
return
|
||||
|
||||
if args.file:
|
||||
# Read message from file
|
||||
with open(args.file, 'r') as f:
|
||||
message = f.read()
|
||||
commit_with_file(message)
|
||||
elif args.message:
|
||||
# Check if message has dangerous patterns
|
||||
safety = check_commit_message_safety(args.message)
|
||||
if safety['safe']:
|
||||
commit_safe(args.message, use_file=False)
|
||||
else:
|
||||
print("⚠️ Message contains potentially dangerous patterns")
|
||||
print("Using file-based commit for safety...")
|
||||
commit_safe(args.message, use_file=True)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,159 +0,0 @@
|
||||
# Safe Commit Practices
|
||||
|
||||
**Issue:** #1430 - [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message
|
||||
|
||||
## Problem
|
||||
|
||||
During commit for #1124, the commit message contained Python code examples that triggered shell execution of memory_mine.py. The backtick-wrapped code in the commit message was interpreted by the shell during git commit processing.
|
||||
|
||||
This is a potential vector for unintended code execution.
|
||||
|
||||
## Safe Commit Methods
|
||||
|
||||
### 1. Use `git commit -F <file>` (Recommended)
|
||||
|
||||
The safest way to commit messages containing code or special characters:
|
||||
|
||||
```bash
|
||||
# Create a file with your commit message
|
||||
echo "Fix: implement memory_mine.py with backtick example
|
||||
|
||||
Example: \`python3 bin/memory_mine.py --days 7\`
|
||||
|
||||
This commit adds memory mining functionality." > /tmp/commit-msg.txt
|
||||
|
||||
# Commit using the file
|
||||
git commit -F /tmp/commit-msg.txt
|
||||
```
|
||||
|
||||
### 2. Use the Safe Commit Tool
|
||||
|
||||
```bash
|
||||
# Safe commit with automatic escaping
|
||||
python3 bin/safe_commit.py -m "Fix: implement memory_mine.py with backtick example"
|
||||
|
||||
# Safe commit using file
|
||||
python3 bin/safe_commit.py -F /tmp/commit-msg.txt
|
||||
|
||||
# Check if a message is safe
|
||||
python3 bin/safe_commit.py --check -m "Example: \`python3 bin/memory_mine.py\`"
|
||||
```
|
||||
|
||||
### 3. Escape Shell Characters Manually
|
||||
|
||||
If you must use `git commit -m`, escape special characters:
|
||||
|
||||
```bash
|
||||
# Escape backticks and other shell characters
|
||||
git commit -m "Fix: implement memory_mine.py with backtick example
|
||||
|
||||
Example: \\`python3 bin/memory_mine.py --days 7\\`
|
||||
|
||||
This commit adds memory mining functionality."
|
||||
```
|
||||
|
||||
## Dangerous Patterns to Avoid
|
||||
|
||||
The following patterns in commit messages can trigger shell execution:
|
||||
|
||||
- **Backticks**: `` `command` `` → Executes command
|
||||
- **Command substitution**: `$(command)` → Executes command
|
||||
- **Variable expansion**: `${variable}` → Expands variable
|
||||
- **Pipes**: `command1 | command2` → Pipes output
|
||||
- **Operators**: `&&`, `||`, `;` → Command chaining
|
||||
- **Redirects**: `>`, `<` → File operations
|
||||
|
||||
## Installation
|
||||
|
||||
### Install the Commit Hook
|
||||
|
||||
To automatically warn about dangerous patterns:
|
||||
|
||||
```bash
|
||||
# Install the commit-msg hook
|
||||
python3 bin/safe_commit.py --install-hook
|
||||
|
||||
# Or manually
|
||||
cp .githooks/commit-msg .git/hooks/commit-msg
|
||||
chmod +x .git/hooks/commit-msg
|
||||
```
|
||||
|
||||
### Configure Git Hooks Path
|
||||
|
||||
If using the `.githooks` directory:
|
||||
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### ❌ Dangerous (Don't do this)
|
||||
|
||||
```bash
|
||||
# This could trigger shell execution
|
||||
git commit -m "Fix: implement memory_mine.py
|
||||
|
||||
Example: \`python3 bin/memory_mine.py --days 7\`
|
||||
|
||||
This mines sessions into MemPalace."
|
||||
```
|
||||
|
||||
### ✅ Safe (Do this instead)
|
||||
|
||||
```bash
|
||||
# Method 1: Use file
|
||||
echo "Fix: implement memory_mine.py
|
||||
|
||||
Example: \`python3 bin/memory_mine.py --days 7\`
|
||||
|
||||
This mines sessions into MemPalace." > /tmp/commit-msg.txt
|
||||
git commit -F /tmp/commit-msg.txt
|
||||
|
||||
# Method 2: Use safe commit tool
|
||||
python3 bin/safe_commit.py -m "Fix: implement memory_mine.py
|
||||
|
||||
Example: \`python3 bin/memory_mine.py --days 7\`
|
||||
|
||||
This mines sessions into MemPalace."
|
||||
|
||||
# Method 3: Escape manually
|
||||
git commit -m "Fix: implement memory_mine.py
|
||||
|
||||
Example: \\`python3 bin/memory_mine.py --days 7\\`
|
||||
|
||||
This mines sessions into MemPalace."
|
||||
```
|
||||
|
||||
## What Happened in Issue #1430
|
||||
|
||||
During commit for #1124, a commit message contained:
|
||||
```
|
||||
Example: \`python3 bin/memory_mine.py --days 7\`
|
||||
```
|
||||
|
||||
The backticks were interpreted by the shell during git commit processing, causing memory_mine.py to execute. While the outcome was positive (26 sessions mined), this is a security risk.
|
||||
|
||||
## Prevention
|
||||
|
||||
1. **Always use `git commit -F <file>`** for messages containing code
|
||||
2. **Install the commit-msg hook** to warn about dangerous patterns
|
||||
3. **Use the safe_commit.py tool** for automatic escaping
|
||||
4. **Document safe patterns** in team guidelines
|
||||
|
||||
## Related Issues
|
||||
|
||||
- **Issue #1430:** This improvement
|
||||
- **Issue #1124:** Original issue that triggered the problem
|
||||
|
||||
## Files
|
||||
|
||||
- `bin/safe_commit.py` - Safe commit tool
|
||||
- `.githooks/commit-msg` - Commit hook (to be installed)
|
||||
- `docs/safe-commit-practices.md` - This documentation
|
||||
|
||||
## Conclusion
|
||||
|
||||
Shell injection in commit messages is a real security risk. By using safe commit practices, we can prevent unintended code execution while still allowing code examples in commit messages.
|
||||
|
||||
**Remember:** When in doubt, use `git commit -F <file>` instead of `git commit -m`.
|
||||
@@ -1,10 +1,69 @@
|
||||
const { app, BrowserWindow, ipcMain } = require('electron')
|
||||
const { exec } = require('child_process')
|
||||
const { execFile } = require('child_process')
|
||||
const path = require('path')
|
||||
|
||||
// MemPalace integration
|
||||
ipcMain.handle('exec-python', (event, command) => {
|
||||
// MemPalace command whitelist — only these operations are allowed
|
||||
const MEMPALACE_BIN = path.resolve(__dirname, 'scripts/mempalace-runner.sh')
|
||||
const ALLOWED_MEMPALACE_OPS = new Set([
|
||||
'init', 'mine', 'search', 'status', 'add_drawer', 'list', 'export'
|
||||
])
|
||||
|
||||
// Validate MemPalace arguments — reject shell metacharacters
|
||||
function sanitizeArg(arg) {
|
||||
if (typeof arg !== 'string') throw new Error('Argument must be a string')
|
||||
// Block shell metacharacters that could enable injection
|
||||
if (/[;&|`$(){}\\n\\r]/.test(arg)) {
|
||||
throw new Error('Invalid characters in argument')
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
// MemPalace integration — safe IPC bridge
|
||||
// Uses execFile (no shell) with argument arrays to prevent command injection
|
||||
ipcMain.handle('mempalace-exec', (event, { op, args }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
// Validate operation
|
||||
if (!ALLOWED_MEMPALACE_OPS.has(op)) {
|
||||
return reject(new Error(`MemPalace operation '${op}' not allowed`))
|
||||
}
|
||||
|
||||
// Sanitize all arguments
|
||||
const safeArgs = [op]
|
||||
if (Array.isArray(args)) {
|
||||
for (const arg of args) {
|
||||
safeArgs.push(sanitizeArg(String(arg)))
|
||||
}
|
||||
}
|
||||
|
||||
// Execute with execFile — no shell interpolation
|
||||
execFile('mempalace', safeArgs, { timeout: 30000 }, (error, stdout, stderr) => {
|
||||
if (error) return reject(error)
|
||||
resolve({ stdout, stderr })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Legacy bridge — DEPRECATED, kept for backward compat but restricted
|
||||
// Only allows predefined commands, no arbitrary execution
|
||||
ipcMain.handle('exec-python', (event, command) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Reject anything that looks like arbitrary shell execution
|
||||
if (typeof command !== 'string') return reject(new Error('Command must be a string'))
|
||||
if (/[;&|`$(){}\\n\\r]/.test(command)) {
|
||||
return reject(new Error('Shell metacharacters not allowed'))
|
||||
}
|
||||
// Only allow mempalace commands
|
||||
if (!command.startsWith('mempalace ')) {
|
||||
return reject(new Error('Only mempalace commands are allowed'))
|
||||
}
|
||||
// Parse into safe argument array
|
||||
const parts = command.split(/\s+/)
|
||||
const op = parts[1]
|
||||
if (!ALLOWED_MEMPALACE_OPS.has(op)) {
|
||||
return reject(new Error(`Operation '${op}' not in whitelist`))
|
||||
}
|
||||
const safeArgs = parts.slice(1).map(sanitizeArg)
|
||||
execFile('mempalace', safeArgs, { timeout: 30000 }, (error, stdout, stderr) => {
|
||||
if (error) return reject(error)
|
||||
resolve({ stdout, stderr })
|
||||
})
|
||||
|
||||
51
mempalace.js
51
mempalace.js
@@ -1,4 +1,4 @@
|
||||
// MemPalace integration
|
||||
// MemPalace integration — uses safe IPC bridge (mempalace-exec)
|
||||
class MemPalace {
|
||||
constructor() {
|
||||
this.palacePath = '~/.mempalace/palace';
|
||||
@@ -6,6 +6,16 @@ class MemPalace {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Safe IPC call — no shell interpolation, uses argument arrays
|
||||
async _exec(op, ...args) {
|
||||
if (window.electronAPI?.mempalaceExec) {
|
||||
return window.electronAPI.mempalaceExec({ op, args })
|
||||
}
|
||||
// Fallback for non-Electron contexts (web-only)
|
||||
console.warn('MemPalace: electronAPI not available, skipping:', op)
|
||||
return { stdout: '', stderr: '' }
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.setupWing();
|
||||
@@ -16,29 +26,46 @@ class MemPalace {
|
||||
}
|
||||
|
||||
async setupWing() {
|
||||
await window.electronAPI.execPython(`mempalace init ${this.palacePath}`);
|
||||
await window.electronAPI.execPython(`mempalace mine ~/chats --mode convos --wing ${this.wing}`);
|
||||
await this._exec('init', this.palacePath);
|
||||
await this._exec('mine', '~/chats', '--mode', 'convos', '--wing', this.wing);
|
||||
}
|
||||
|
||||
setupAutoMining() {
|
||||
setInterval(() => {
|
||||
window.electronAPI.execPython(`mempalace mine #chat-container --mode convos --wing ${this.wing}`);
|
||||
this._exec('mine', '#chat-container', '--mode', 'convos', '--wing', this.wing);
|
||||
}, 30000); // Mine every 30 seconds
|
||||
}
|
||||
|
||||
async search(query) {
|
||||
const result = await window.electronAPI.execPython(`mempalace search "${query}" --wing ${this.wing}`);
|
||||
const result = await this._exec('search', query, '--wing', this.wing);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
async addDrawer(wing, drawer, content) {
|
||||
return this._exec('add_drawer', wing, drawer, content);
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
const stats = window.electronAPI.execPython(`mempalace status --wing ${this.wing}`);
|
||||
document.getElementById('compression-ratio').textContent =
|
||||
`${stats.compression_ratio.toFixed(1)}x`;
|
||||
document.getElementById('docs-mined').textContent = stats.total_docs;
|
||||
document.getElementById('aaak-size').textContent = stats.aaak_size;
|
||||
this._exec('status', '--wing', this.wing).then(stats => {
|
||||
// stats comes as JSON string from stdout
|
||||
try {
|
||||
const data = typeof stats.stdout === 'string' ? JSON.parse(stats.stdout) : stats
|
||||
const crEl = document.getElementById('compression-ratio');
|
||||
const dmEl = document.getElementById('docs-mined');
|
||||
const akEl = document.getElementById('aaak-size');
|
||||
if (crEl) crEl.textContent = `${(data.compression_ratio || 0).toFixed(1)}x`;
|
||||
if (dmEl) dmEl.textContent = data.total_docs || 0;
|
||||
if (akEl) akEl.textContent = data.aaak_size || 0;
|
||||
} catch (e) {
|
||||
console.error('MemPalace stats parse error:', e);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('MemPalace stats error:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize MemPalace
|
||||
const mempalace = new MemPalace();
|
||||
// Initialize MemPalace only in Electron context
|
||||
if (typeof window !== 'undefined' && window.electronAPI) {
|
||||
const mempalace = new MemPalace();
|
||||
}
|
||||
|
||||
11
preload.js
Normal file
11
preload.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// preload.js — Electron context bridge
|
||||
// Safely exposes IPC methods to the renderer process
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Safe MemPalace execution — uses argument arrays, no shell strings
|
||||
mempalaceExec: (opts) => ipcRenderer.invoke('mempalace-exec', opts),
|
||||
|
||||
// Legacy bridge — restricted to whitelisted mempalace commands only
|
||||
execPython: (command) => ipcRenderer.invoke('exec-python', command),
|
||||
})
|
||||
68
tests/test_electron_security.py
Normal file
68
tests/test_electron_security.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Test that electron-main.js security fix prevents command injection."""
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
def test_no_raw_exec():
|
||||
"""electron-main.js should not use child_process.exec (only execFile)."""
|
||||
content = open('electron-main.js').read()
|
||||
# Should use execFile, not exec
|
||||
assert 'execFile' in content, "Should use execFile for safe subprocess execution"
|
||||
# Should NOT have raw exec import (allow execFile which contains 'exec')
|
||||
lines = content.split('\n')
|
||||
for line in lines:
|
||||
if 'require' in line and 'exec' in line:
|
||||
assert 'execFile' in line, f"Should import execFile, not exec: {line}"
|
||||
|
||||
def test_command_whitelist():
|
||||
"""electron-main.js should have a whitelist of allowed operations."""
|
||||
content = open('electron-main.js').read()
|
||||
assert 'ALLOWED_MEMPALACE_OPS' in content, "Should have operation whitelist"
|
||||
assert 'search' in content, "Whitelist should include 'search'"
|
||||
assert 'init' in content, "Whitelist should include 'init'"
|
||||
|
||||
def test_shell_metacharacter_rejection():
|
||||
"""electron-main.js should reject shell metacharacters in arguments."""
|
||||
content = open('electron-main.js').read()
|
||||
assert 'sanitizeArg' in content, "Should have argument sanitizer"
|
||||
assert 'metacharacter' in content.lower() or 'metacharacters' in content.lower() or '[;&|`$()' in content, \
|
||||
"Should check for shell metacharacters"
|
||||
|
||||
def test_mempalace_no_template_interpolation():
|
||||
"""mempalace.js should not use template literals with shell commands."""
|
||||
content = open('mempalace.js').read()
|
||||
# Should NOT have backtick strings with shell commands
|
||||
dangerous_patterns = re.findall(r'`mempalace\s', content)
|
||||
assert len(dangerous_patterns) == 0, \
|
||||
f"mempalace.js should not have template-interpolated shell commands, found: {dangerous_patterns}"
|
||||
|
||||
def test_mempalace_uses_safe_ipc():
|
||||
"""mempalace.js should use the safe mempalace-exec IPC."""
|
||||
content = open('mempalace.js').read()
|
||||
assert 'mempalaceExec' in content or '_exec' in content, \
|
||||
"mempalace.js should use safe IPC method"
|
||||
assert 'execPython' not in content, \
|
||||
"mempalace.js should not reference the legacy execPython"
|
||||
|
||||
def test_app_no_template_interpolation():
|
||||
"""app.js should not have template-interpolated shell commands."""
|
||||
content = open('app.js').read()
|
||||
dangerous = re.findall(r'`mempalace\s', content)
|
||||
assert len(dangerous) == 0, \
|
||||
f"app.js should not have template-interpolated mempalace commands, found: {dangerous}"
|
||||
|
||||
def test_preload_exposes_safe_api():
|
||||
"""preload.js should expose mempalaceExec through context bridge."""
|
||||
content = open('preload.js').read()
|
||||
assert 'mempalaceExec' in content, "preload.js should expose mempalaceExec"
|
||||
assert 'contextBridge' in content, "preload.js should use contextBridge"
|
||||
assert 'exec-python' in content, "preload.js should still expose legacy exec-python bridge"
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_no_raw_exec()
|
||||
test_command_whitelist()
|
||||
test_shell_metacharacter_rejection()
|
||||
test_mempalace_no_template_interpolation()
|
||||
test_mempalace_uses_safe_ipc()
|
||||
test_app_no_template_interpolation()
|
||||
test_preload_exposes_safe_api()
|
||||
print("All security tests passed")
|
||||
Reference in New Issue
Block a user