feat: add YOLO Mode for Jupyter notebooks

- Add comprehensive security validation for Jupyter notebooks
- Implement YOLO Mode setting to bypass security restrictions in trusted environments
- Add jupyterNotebookYoloMode configuration option
- Create JupyterNotebookSecurity class with risk detection for:
  - Critical risks: eval, exec, shell commands
  - High risks: dangerous imports, script injection
  - Medium risks: file/network operations
- Implement automatic sanitization of dangerous content
- Add read-only mode enforcement for risky notebooks
- Include comprehensive test suite with 27 test cases
- Add detailed documentation for security features

This allows users to work with trusted notebooks containing "unsafe" constructs
by enabling YOLO Mode in settings when needed.
This commit is contained in:
Roo Code 2025-09-03 21:05:27 +00:00
parent b486282f6b
commit 24b0f76ff1
8 changed files with 1468 additions and 13 deletions

View file

@ -0,0 +1,296 @@
# Jupyter Notebook Security Features
## Overview
Roo Code includes comprehensive security features for working with Jupyter notebooks (`.ipynb` files). These features protect against potentially dangerous code execution, cross-site scripting (XSS) attacks, and other security risks commonly associated with Jupyter notebooks.
## Security Validation
### Automatic Risk Detection
When opening or editing Jupyter notebooks, Roo Code automatically scans for:
#### Critical Risks (Blocks Editing)
- **Code Execution**: `eval()`, `exec()`, `compile()`, `__import__()`
- **Shell Commands**: `!command`, `%system`, `%%bash`, `%%sh`, `%%script`
#### High Risks (Blocks Editing)
- **Dangerous Imports**: `subprocess`, `os`, `sys`, `socket`, `pickle`
- **Script Injection**: `<script>` tags in markdown cells
- **IFrame Injection**: `<iframe>` tags in markdown cells
- **JavaScript Protocol**: `javascript:` URLs in markdown
- **JavaScript Outputs**: Cells with `application/javascript` output
#### Medium Risks (Warnings Only)
- **File Operations**: `open()`, `file()`, `.read()`, `.write()`
- **Network Operations**: `requests`, `urllib`, `http.client`
- **Event Handlers**: `onclick`, `onload`, etc. in markdown
- **Oversized Cells**: Cells exceeding 1MB
- **Excessive Cell Count**: Notebooks with >1000 cells
#### Low Risks (Informational)
- **Suspicious Metadata**: Presence of `widgets`, `extensions`, or `jupyter_dashboards` fields
### Read-Only Mode
Notebooks containing critical or high-severity risks automatically open in read-only mode, preventing:
- Cell modifications
- Cell additions or deletions
- Saving changes to disk
## YOLO Mode (You Only Live Once)
For trusted environments where you need to work with notebooks containing "unsafe" constructs, YOLO Mode allows you to bypass all security restrictions.
### Enabling YOLO Mode
Add the following to your VSCode settings:
```json
{
"roo-code.jupyterNotebookYoloMode": true
}
```
Or use the command palette:
1. Press `Cmd/Ctrl + Shift + P`
2. Search for "Preferences: Open Settings (JSON)"
3. Add the setting above
### What YOLO Mode Does
When enabled, YOLO Mode:
- ✅ Bypasses all security validation
- ✅ Allows editing of any notebook content
- ✅ Permits execution of shell commands
- ✅ Allows dangerous imports and operations
- ✅ Enables saving of modified notebooks
### ⚠️ YOLO Mode Warnings
**USE WITH EXTREME CAUTION**: YOLO Mode completely disables security protections. Only enable it when:
- Working with trusted notebooks from known sources
- In isolated/sandboxed environments
- You fully understand the risks involved
- The notebook requires system-level operations for legitimate purposes
## Security Configuration
### Advanced Configuration Options
```typescript
interface JupyterSecurityConfig {
// Enable YOLO Mode (bypass all security)
yoloMode?: boolean
// Allow code execution (default: false)
allowCodeExecution?: boolean
// Force read-only mode (default: true)
readOnlyMode?: boolean
// Maximum cell size in bytes (default: 1MB)
maxCellSize?: number
// Maximum number of cells (default: 1000)
maxCellCount?: number
// Trusted source paths (notebooks from these paths bypass security)
trustedSources?: string[]
}
```
### Trusted Sources
You can configure specific directories as trusted sources. Notebooks from these locations will bypass security checks:
```json
{
"roo-code.jupyterTrustedSources": ["/home/user/trusted-notebooks", "/projects/data-science/verified"]
}
```
## Automatic Sanitization
When security risks are detected and YOLO Mode is disabled, Roo Code automatically sanitizes dangerous content:
### Code Cell Sanitization
- Dangerous code is commented out with security warnings
- Original code is preserved as comments for reference
- Cell outputs are cleared
- Execution count is reset
Example:
```python
# ⚠️ SECURITY WARNING: This cell has been disabled due to security risks (eval, exec)
# To run this cell, enable YOLO Mode in settings
# Original code:
# eval('__import__("os").system("rm -rf /")')
```
### Markdown Cell Sanitization
- Script tags are replaced with `[REMOVED: script tag]`
- IFrame tags are replaced with `[REMOVED: iframe]`
- JavaScript protocols are replaced with `[REMOVED]:`
- Event handlers are prefixed with `data-removed=`
### Output Sanitization
- HTML outputs containing scripts are removed
- JavaScript outputs are replaced with plain text warnings
- Data URIs with HTML content are flagged
## Best Practices
### For Notebook Authors
1. **Avoid dangerous patterns**: Use subprocess.run() instead of eval/exec
2. **Validate inputs**: Never execute user input directly
3. **Use virtual environments**: Isolate notebook dependencies
4. **Document requirements**: Clearly state what system access is needed
5. **Test in sandboxes**: Verify notebooks work in restricted environments
### For Notebook Users
1. **Review before running**: Always inspect notebook content before execution
2. **Use YOLO Mode sparingly**: Only for trusted, verified notebooks
3. **Work in containers**: Use Docker or VMs for untrusted notebooks
4. **Check outputs**: Review cell outputs for suspicious content
5. **Keep backups**: Save important data before running unknown notebooks
## Security Risk Examples
### Critical Risk Example
```python
# This will be blocked
eval('__import__("os").system("cat /etc/passwd")')
exec(user_input) # Never execute user input
!rm -rf / # Shell commands are blocked
```
### High Risk Example
```python
# These imports will trigger read-only mode
import subprocess
import os
import socket
# Markdown with scripts
"""
<script>alert('XSS')</script>
<iframe src="http://malicious.site"></iframe>
"""
```
### Medium Risk Example
```python
# These will show warnings but allow editing
with open('/etc/hosts', 'r') as f:
data = f.read()
import requests
response = requests.get('https://api.example.com')
```
## Troubleshooting
### "Cannot modify notebook: Security risks detected"
- **Cause**: The notebook contains critical or high-severity security risks
- **Solution**: Enable YOLO Mode if you trust the notebook source
### "Notebook is in read-only mode"
- **Cause**: Security validation detected dangerous patterns
- **Solution**: Review the security warnings and enable YOLO Mode if needed
### "Cell has been disabled due to security risks"
- **Cause**: Automatic sanitization has commented out dangerous code
- **Solution**: Review the code and enable YOLO Mode to restore functionality
## API Integration
### Programmatic Security Validation
```typescript
import { JupyterNotebookSecurity } from "./jupyter-notebook-security"
import { JupyterNotebookHandler } from "./jupyter-notebook-handler"
// Create security validator
const security = new JupyterNotebookSecurity({
yoloMode: false,
maxCellSize: 1024 * 1024,
trustedSources: ["/trusted/path"],
})
// Load and validate notebook
const handler = await JupyterNotebookHandler.fromFile("notebook.ipynb", { yoloMode: false })
// Check security status
if (handler.hasSecurityRisks()) {
const risks = handler.getSecurityRisks()
console.log("Security risks detected:", risks)
}
// Check if editing is allowed
if (handler.isReadOnly()) {
console.log("Notebook is in read-only mode")
}
```
### Enabling YOLO Mode Programmatically
```typescript
// Enable YOLO Mode for a specific notebook
handler.setYoloMode(true)
// Now editing is allowed regardless of security risks
handler.updateCell(0, 'eval("now this works")')
await handler.save()
```
## Security Compliance
This implementation follows security best practices:
- **Defense in Depth**: Multiple layers of security validation
- **Fail Secure**: Defaults to read-only mode when risks are detected
- **Transparency**: Clear warnings about detected risks
- **User Control**: YOLO Mode for informed consent
- **Preservation**: Original content is preserved during sanitization
## Future Enhancements
Planned improvements to the security system:
- [ ] Configurable risk severity levels
- [ ] Custom pattern definitions
- [ ] Sandbox execution environment
- [ ] Digital signatures for trusted notebooks
- [ ] Security audit logs
- [ ] Integration with corporate security policies
## Support
For questions or issues related to Jupyter notebook security:
1. Check this documentation
2. Review the security warnings in the UI
3. Open an issue on GitHub with the `jupyter-security` label
4. Contact the security team for enterprise deployments

View file

@ -152,6 +152,9 @@ export const globalSettingsSchema = z.object({
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),
lastModeImportPath: z.string().optional(),
// Jupyter notebook security settings
jupyterNotebookYoloMode: z.boolean().optional(),
})
export type GlobalSettings = z.infer<typeof globalSettingsSchema>

View file

@ -1,14 +1,17 @@
import { DiffStrategy, DiffResult, ToolUse } from "../../../shared/tools"
import { ToolProgressStatus } from "@roo-code/types"
import { ToolProgressStatus, GlobalState } from "@roo-code/types"
import { JupyterNotebookHandler } from "../../../integrations/misc/jupyter-notebook-handler"
import { JupyterSecurityConfig } from "../../../integrations/misc/jupyter-notebook-security"
import { MultiSearchReplaceDiffStrategy } from "./multi-search-replace"
export class JupyterNotebookDiffStrategy implements DiffStrategy {
private fallbackStrategy: MultiSearchReplaceDiffStrategy
private globalState?: GlobalState
constructor(fuzzyThreshold?: number, bufferLines?: number) {
constructor(fuzzyThreshold?: number, bufferLines?: number, globalState?: GlobalState) {
// Use MultiSearchReplaceDiffStrategy as fallback for non-cell operations
this.fallbackStrategy = new MultiSearchReplaceDiffStrategy(fuzzyThreshold, bufferLines)
this.globalState = globalState
}
getName(): string {
@ -122,12 +125,28 @@ Your cell operation or search/replace content here
// Check if this is a Jupyter notebook by trying to parse it
let handler: JupyterNotebookHandler
try {
handler = new JupyterNotebookHandler("", originalContent)
// Create security config based on global settings
const securityConfig: JupyterSecurityConfig = {
yoloMode: this.globalState?.jupyterNotebookYoloMode === true,
allowCodeExecution: this.globalState?.jupyterNotebookYoloMode === true,
readOnlyMode: this.globalState?.jupyterNotebookYoloMode !== true,
}
handler = new JupyterNotebookHandler("", originalContent, securityConfig)
} catch (error) {
// Not a valid notebook, fall back to standard diff
return this.fallbackStrategy.applyDiff(originalContent, diffContent, _paramStartLine, _paramEndLine)
}
// Check if notebook is in read-only mode due to security
if (handler.isReadOnly()) {
const risks = handler.getSecurityRisks()
const riskSummary = risks.map((r) => `${r.severity}: ${r.description}`).join(", ")
return {
success: false,
error: `Cannot modify notebook: Security risks detected (${riskSummary}). Enable YOLO Mode in settings to bypass security restrictions.`,
}
}
// Check if this is a cell operation
const cellOperationMatch = diffContent.match(
/<<<<<<< CELL_OPERATION\s*\n(?::operation:\s*(edit|add|delete)\s*\n)?(?::cell_index:\s*(\d+)\s*\n)?(?::cell_type:\s*(code|markdown|raw)\s*\n)?(?:-------\s*\n)?([\s\S]*?)(?:\n)?=======\s*\n([\s\S]*?)(?:\n)?>>>>>>> CELL_OPERATION/,

View file

@ -2705,17 +2705,23 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const workspaceDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
const hasJupyterFiles = workspaceDir && (await this.checkForJupyterFiles(workspaceDir))
// Get the provider and state for accessing global settings
const provider = this.providerRef.deref()
const state = provider ? await provider.getState() : undefined
if (hasJupyterFiles) {
// Use Jupyter-specific diff strategy for notebooks
this.diffStrategy = new JupyterNotebookDiffStrategy(this.fuzzyMatchThreshold)
this.diffStrategy = new JupyterNotebookDiffStrategy(
this.fuzzyMatchThreshold,
undefined, // bufferLines
state, // Pass global state for security settings
)
} else {
// Default to old strategy, will be updated if experiment is enabled.
this.diffStrategy = new MultiSearchReplaceDiffStrategy(this.fuzzyMatchThreshold)
// Check if the multi-file apply diff experiment is enabled
const provider = this.providerRef.deref()
if (provider) {
const state = await provider.getState()
if (provider && state) {
const isMultiFileApplyDiffEnabled = experiments.isEnabled(
state.experiments ?? {},
EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,

View file

@ -0,0 +1,617 @@
import { describe, it, expect, beforeEach } from "vitest"
import { JupyterNotebookSecurity, JupyterSecurityConfig } from "../jupyter-notebook-security"
import { JupyterNotebook } from "../jupyter-notebook-handler"
describe("JupyterNotebookSecurity", () => {
let security: JupyterNotebookSecurity
let sampleNotebook: JupyterNotebook
beforeEach(() => {
security = new JupyterNotebookSecurity()
sampleNotebook = {
cells: [
{
cell_type: "code",
source: "print('Hello, World!')",
metadata: {},
outputs: [],
execution_count: 1,
},
{
cell_type: "markdown",
source: "# Safe Markdown\nThis is safe content.",
metadata: {},
},
],
metadata: {
kernelspec: {
display_name: "Python 3",
language: "python",
name: "python3",
},
},
nbformat: 4,
nbformat_minor: 5,
}
})
describe("Safe notebooks", () => {
it("should validate safe notebook as secure", () => {
const result = security.validateNotebook(sampleNotebook)
expect(result.isSecure).toBe(true)
expect(result.risks).toHaveLength(0)
expect(result.requiresReadOnly).toBe(false)
})
it("should allow basic Python operations", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "import numpy as np\nimport pandas as pd\ndata = [1, 2, 3]\nprint(sum(data))",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(true)
expect(result.risks).toHaveLength(0)
})
})
describe("Dangerous code patterns", () => {
it("should detect eval() usage", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "result = eval('2 + 2')",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "critical",
type: "eval",
}),
)
})
it("should detect exec() usage", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "exec('import os')",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "critical",
type: "exec",
}),
)
})
it("should detect shell commands", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "!rm -rf /",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "critical",
type: "shell_command",
}),
)
})
it("should detect dangerous imports", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "import subprocess\nsubprocess.run(['ls', '-la'])",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "high",
type: "subprocess_import",
}),
)
})
it("should detect file operations", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "with open('/etc/passwd', 'r') as f:\n content = f.read()",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(true) // Medium risk doesn't make it insecure
expect(result.requiresReadOnly).toBe(false)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "medium",
type: "file_open",
}),
)
})
it("should detect network operations", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "import requests\nresponse = requests.get('http://example.com')",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(true) // Medium risk doesn't make it insecure
expect(result.requiresReadOnly).toBe(false)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "medium",
type: "network_request",
}),
)
})
})
describe("Dangerous markdown patterns", () => {
it("should detect script tags in markdown", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "markdown",
source: "# Title\n<script>alert('XSS')</script>",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "high",
type: "script_tag",
}),
)
})
it("should detect iframe tags", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "markdown",
source: "<iframe src='http://malicious.com'></iframe>",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "high",
type: "iframe_tag",
}),
)
})
it("should detect javascript: protocol", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "markdown",
source: "[Click me](javascript:alert('XSS'))",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "high",
type: "javascript_protocol",
}),
)
})
})
describe("Output validation", () => {
it("should detect dangerous HTML outputs", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "print('safe')",
metadata: {},
outputs: [
{
data: {
"text/html": "<script>alert('XSS')</script>",
},
},
],
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "high",
type: "output_script",
}),
)
})
it("should detect JavaScript outputs", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "print('safe')",
metadata: {},
outputs: [
{
data: {
"application/javascript": "console.log('executed')",
},
},
],
},
],
}
const result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "high",
type: "javascript_output",
}),
)
})
})
describe("YOLO Mode", () => {
it("should bypass all security checks when YOLO Mode is enabled", () => {
const yoloSecurity = new JupyterNotebookSecurity({ yoloMode: true })
const dangerousNotebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: 'eval(\'__import__("os").system("rm -rf /")\')',
metadata: {},
},
{
cell_type: "markdown",
source: "<script>alert('XSS')</script>",
metadata: {},
},
],
}
const result = yoloSecurity.validateNotebook(dangerousNotebook)
expect(result.isSecure).toBe(true)
expect(result.risks).toHaveLength(0)
expect(result.requiresReadOnly).toBe(false)
})
it("should allow updating YOLO Mode configuration", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "eval('2+2')",
metadata: {},
},
],
}
// Initially, security is enforced
let result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
// Enable YOLO Mode
security.updateConfig({ yoloMode: true })
result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(true)
expect(result.requiresReadOnly).toBe(false)
// Disable YOLO Mode
security.updateConfig({ yoloMode: false })
result = security.validateNotebook(notebook)
expect(result.isSecure).toBe(false)
expect(result.requiresReadOnly).toBe(true)
})
it("should correctly report YOLO Mode status", () => {
expect(security.isYoloModeEnabled()).toBe(false)
security.updateConfig({ yoloMode: true })
expect(security.isYoloModeEnabled()).toBe(true)
security.updateConfig({ yoloMode: false })
expect(security.isYoloModeEnabled()).toBe(false)
})
})
describe("Sanitization", () => {
it("should sanitize dangerous code cells", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "eval('malicious code')",
metadata: {},
outputs: [{ data: { "text/plain": "output" } }],
execution_count: 1,
},
],
}
const result = security.validateNotebook(notebook)
expect(result.sanitizedNotebook).toBeDefined()
const sanitizedCell = result.sanitizedNotebook!.cells[0]
expect(sanitizedCell.source).toContain("SECURITY WARNING")
expect(sanitizedCell.source).toContain("eval")
expect(sanitizedCell.outputs).toHaveLength(0)
expect(sanitizedCell.execution_count).toBeNull()
})
it("should sanitize dangerous markdown cells", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "markdown",
source: "# Title\n<script>alert('XSS')</script>\n<iframe src='bad'></iframe>",
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.sanitizedNotebook).toBeDefined()
const sanitizedCell = result.sanitizedNotebook!.cells[0]
expect(sanitizedCell.source).toContain("[REMOVED: script tag]")
expect(sanitizedCell.source).toContain("[REMOVED: iframe]")
expect(sanitizedCell.source).not.toContain("<script>")
expect(sanitizedCell.source).not.toContain("<iframe")
})
it("should remove dangerous outputs", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "import os", // High risk to trigger sanitization
metadata: {},
outputs: [
{
data: {
"text/html": "<script>alert('XSS')</script>",
"application/javascript": "console.log('bad')",
},
},
],
},
],
}
const result = security.validateNotebook(notebook)
expect(result.sanitizedNotebook).toBeDefined()
// The cell should be sanitized because it has high-risk import
const sanitizedCell = result.sanitizedNotebook!.cells[0]
expect(sanitizedCell.source).toContain("SECURITY WARNING")
// Outputs should be cleared for dangerous cells
expect(sanitizedCell.outputs).toHaveLength(0)
})
it("should sanitize dangerous outputs in safe cells", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "print('safe code')", // Safe code
metadata: {},
outputs: [
{
data: {
"text/html": "<div>Safe HTML</div>",
"text/plain": ["safe output"],
},
},
{
data: {
"text/html": "<script>alert('XSS')</script>",
"application/javascript": "console.log('bad')",
},
},
],
},
],
}
const result = security.validateNotebook(notebook)
// Since the outputs have high-risk content (script tags), it should be flagged
expect(result.isSecure).toBe(false)
expect(result.sanitizedNotebook).toBeDefined()
// The cell should be disabled because it has high-risk outputs
const sanitizedCell = result.sanitizedNotebook!.cells[0]
expect(sanitizedCell.source).toContain("SECURITY WARNING")
expect(sanitizedCell.outputs).toHaveLength(0)
})
it("should not sanitize when YOLO Mode is enabled", () => {
const yoloSecurity = new JupyterNotebookSecurity({ yoloMode: true })
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "eval('dangerous')",
metadata: {},
},
],
}
const result = yoloSecurity.validateNotebook(notebook)
expect(result.sanitizedNotebook).toBeUndefined()
})
})
describe("Trusted sources", () => {
it("should trust notebooks from trusted sources", () => {
const securityWithTrusted = new JupyterNotebookSecurity({
trustedSources: ["/trusted/path", "/safe/notebooks"],
})
const dangerousNotebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "eval('trusted code')",
metadata: {},
},
],
}
const result = securityWithTrusted.validateNotebook(dangerousNotebook, "/trusted/path/notebook.ipynb")
expect(result.isSecure).toBe(true)
expect(result.risks).toHaveLength(0)
expect(result.requiresReadOnly).toBe(false)
})
it("should not trust notebooks from untrusted sources", () => {
const securityWithTrusted = new JupyterNotebookSecurity({
trustedSources: ["/trusted/path"],
})
const dangerousNotebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "eval('untrusted code')",
metadata: {},
},
],
}
const result = securityWithTrusted.validateNotebook(dangerousNotebook, "/untrusted/path/notebook.ipynb")
expect(result.isSecure).toBe(false)
expect(result.risks.length).toBeGreaterThan(0)
expect(result.requiresReadOnly).toBe(true)
})
})
describe("Size limits", () => {
it("should detect oversized cells", () => {
const largeContent = "x".repeat(2 * 1024 * 1024) // 2MB
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: largeContent,
metadata: {},
},
],
}
const result = security.validateNotebook(notebook)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "medium",
type: "oversized_cell",
}),
)
})
it("should detect excessive cell count", () => {
const cells = Array(1001).fill({
cell_type: "code",
source: "print('cell')",
metadata: {},
})
const notebook: JupyterNotebook = { cells }
const result = security.validateNotebook(notebook)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "medium",
type: "excessive_cells",
}),
)
})
})
describe("Metadata validation", () => {
it("should detect suspicious metadata fields", () => {
const notebook: JupyterNotebook = {
cells: [],
metadata: {
widgets: { some: "data" },
extensions: { another: "data" },
jupyter_dashboards: { layout: "grid" },
},
}
const result = security.validateNotebook(notebook)
expect(result.risks).toHaveLength(3)
expect(result.risks).toContainEqual(
expect.objectContaining({
severity: "low",
type: "suspicious_metadata",
description: expect.stringContaining("widgets"),
}),
)
})
it("should sanitize suspicious metadata", () => {
const notebook: JupyterNotebook = {
cells: [
{
cell_type: "code",
source: "eval('bad')",
metadata: {},
},
],
metadata: {
widgets: { some: "data" },
kernelspec: { name: "python3" },
},
}
const result = security.validateNotebook(notebook)
expect(result.sanitizedNotebook).toBeDefined()
expect(result.sanitizedNotebook!.metadata?.widgets).toBeUndefined()
expect(result.sanitizedNotebook!.metadata?.kernelspec).toBeDefined()
})
})
})

View file

@ -8,6 +8,7 @@ import { extractTextFromXLSX } from "./extract-text-from-xlsx"
import { countFileLines } from "./line-counter"
import { readLines } from "./read-lines"
import { JupyterNotebookHandler } from "./jupyter-notebook-handler"
import { JupyterSecurityConfig } from "./jupyter-notebook-security"
async function extractTextFromPDF(filePath: string): Promise<string> {
const dataBuffer = await fs.readFile(filePath)
@ -20,9 +21,32 @@ async function extractTextFromDOCX(filePath: string): Promise<string> {
return addLineNumbers(result.value)
}
async function extractTextFromIPYNB(filePath: string): Promise<string> {
const handler = await JupyterNotebookHandler.fromFile(filePath)
return handler.extractTextWithCellMarkers()
async function extractTextFromIPYNB(filePath: string, securityConfig?: JupyterSecurityConfig): Promise<string> {
const handler = await JupyterNotebookHandler.fromFile(filePath, securityConfig)
// Add security warning if risks are detected
let result = ""
if (handler.hasSecurityRisks() && !securityConfig?.yoloMode) {
const risks = handler.getSecurityRisks()
const criticalRisks = risks.filter((r) => r.severity === "critical")
const highRisks = risks.filter((r) => r.severity === "high")
result += "# ⚠️ SECURITY WARNING ⚠️\n"
result += "# This Jupyter notebook contains potential security risks:\n"
if (criticalRisks.length > 0) {
result += `# - ${criticalRisks.length} CRITICAL risk(s): ${criticalRisks.map((r) => r.type).join(", ")}\n`
}
if (highRisks.length > 0) {
result += `# - ${highRisks.length} HIGH risk(s): ${highRisks.map((r) => r.type).join(", ")}\n`
}
result += "# The notebook is in READ-ONLY mode. Enable YOLO Mode to bypass restrictions.\n"
result += "# " + "=".repeat(70) + "\n\n"
}
result += handler.extractTextWithCellMarkers()
return result
}
/**

View file

@ -1,6 +1,7 @@
import * as fs from "fs/promises"
import * as path from "path"
import { addLineNumbers } from "./extract-text"
import { JupyterNotebookSecurity, SecurityValidationResult, JupyterSecurityConfig } from "./jupyter-notebook-security"
export interface JupyterCell {
cell_type: "code" | "markdown" | "raw"
@ -29,12 +30,23 @@ export class JupyterNotebookHandler {
private notebook: JupyterNotebook
private filePath: string
private cellReferences: CellReference[] = []
private security: JupyterNotebookSecurity
private securityValidation?: SecurityValidationResult
constructor(filePath: string, notebookContent?: string) {
constructor(filePath: string, notebookContent?: string, securityConfig?: JupyterSecurityConfig) {
this.filePath = filePath
this.security = new JupyterNotebookSecurity(securityConfig)
if (notebookContent) {
this.notebook = JSON.parse(notebookContent)
this.buildCellReferences()
// Validate security on load
this.securityValidation = this.security.validateNotebook(this.notebook, filePath)
// Use sanitized notebook if available and not in YOLO mode
if (this.securityValidation.sanitizedNotebook && !this.security.isYoloModeEnabled()) {
this.notebook = this.securityValidation.sanitizedNotebook
this.buildCellReferences()
}
} else {
this.notebook = { cells: [] }
}
@ -43,9 +55,9 @@ export class JupyterNotebookHandler {
/**
* Load a Jupyter notebook from file
*/
static async fromFile(filePath: string): Promise<JupyterNotebookHandler> {
static async fromFile(filePath: string, securityConfig?: JupyterSecurityConfig): Promise<JupyterNotebookHandler> {
const content = await fs.readFile(filePath, "utf8")
return new JupyterNotebookHandler(filePath, content)
return new JupyterNotebookHandler(filePath, content, securityConfig)
}
/**
@ -140,6 +152,12 @@ export class JupyterNotebookHandler {
* Update a specific cell's content
*/
updateCell(cellIndex: number, newContent: string): boolean {
// Check if read-only mode is enforced
if (this.isReadOnly()) {
console.warn("Cannot update cell: Notebook is in read-only mode due to security restrictions")
return false
}
if (cellIndex < 0 || cellIndex >= this.notebook.cells.length) {
return false
}
@ -167,6 +185,12 @@ export class JupyterNotebookHandler {
* Insert a new cell
*/
insertCell(index: number, cellType: "code" | "markdown" | "raw", content: string): boolean {
// Check if read-only mode is enforced
if (this.isReadOnly()) {
console.warn("Cannot insert cell: Notebook is in read-only mode due to security restrictions")
return false
}
if (index < 0 || index > this.notebook.cells.length) {
return false
}
@ -193,6 +217,12 @@ export class JupyterNotebookHandler {
* Delete a cell
*/
deleteCell(index: number): boolean {
// Check if read-only mode is enforced
if (this.isReadOnly()) {
console.warn("Cannot delete cell: Notebook is in read-only mode due to security restrictions")
return false
}
if (index < 0 || index >= this.notebook.cells.length) {
return false
}
@ -226,6 +256,11 @@ export class JupyterNotebookHandler {
* Save the notebook back to file
*/
async save(): Promise<void> {
// Check if read-only mode is enforced
if (this.isReadOnly()) {
throw new Error("Cannot save notebook: Notebook is in read-only mode due to security restrictions")
}
const content = JSON.stringify(this.notebook, null, 2)
await fs.writeFile(this.filePath, content, "utf8")
}
@ -335,4 +370,41 @@ export class JupyterNotebookHandler {
cells,
}
}
/**
* Get security validation results
*/
getSecurityValidation(): SecurityValidationResult | undefined {
return this.securityValidation
}
/**
* Check if notebook is in read-only mode
*/
isReadOnly(): boolean {
return this.securityValidation?.requiresReadOnly === true && !this.security.isYoloModeEnabled()
}
/**
* Check if notebook has security risks
*/
hasSecurityRisks(): boolean {
return (this.securityValidation?.risks.length ?? 0) > 0
}
/**
* Get security risks
*/
getSecurityRisks() {
return this.securityValidation?.risks || []
}
/**
* Enable or disable YOLO Mode
*/
setYoloMode(enabled: boolean): void {
this.security.updateConfig({ yoloMode: enabled })
// Re-validate with new settings
this.securityValidation = this.security.validateNotebook(this.notebook, this.filePath)
}
}

View file

@ -0,0 +1,418 @@
import { JupyterNotebook, JupyterCell } from "./jupyter-notebook-handler"
export interface SecurityRisk {
severity: "critical" | "high" | "medium" | "low"
type: string
cellIndex?: number
cellType?: string
description: string
pattern?: string
}
export interface SecurityValidationResult {
isSecure: boolean
risks: SecurityRisk[]
requiresReadOnly: boolean
sanitizedNotebook?: JupyterNotebook
}
export interface JupyterSecurityConfig {
allowCodeExecution?: boolean
readOnlyMode?: boolean
maxCellSize?: number
maxCellCount?: number
trustedSources?: string[]
yoloMode?: boolean // New YOLO Mode flag
}
const DEFAULT_CONFIG: JupyterSecurityConfig = {
allowCodeExecution: false,
readOnlyMode: false, // Only enforce read-only when risks are detected
maxCellSize: 1024 * 1024, // 1MB
maxCellCount: 1000,
trustedSources: [],
yoloMode: false,
}
// Dangerous code patterns that could execute arbitrary code
const DANGEROUS_CODE_PATTERNS = [
// Direct code execution
{ pattern: /\beval\s*\(/, severity: "critical" as const, type: "eval" },
{ pattern: /\bexec\s*\(/, severity: "critical" as const, type: "exec" },
{ pattern: /\bcompile\s*\(/, severity: "critical" as const, type: "compile" },
{ pattern: /\b__import__\s*\(/, severity: "critical" as const, type: "__import__" },
// System commands
{ pattern: /^!.*/, severity: "critical" as const, type: "shell_command" },
{ pattern: /%system\s+/, severity: "critical" as const, type: "magic_system" },
{ pattern: /%%bash/, severity: "critical" as const, type: "magic_bash" },
{ pattern: /%%sh/, severity: "critical" as const, type: "magic_sh" },
{ pattern: /%%script/, severity: "critical" as const, type: "magic_script" },
// Dangerous imports
{ pattern: /import\s+subprocess/, severity: "high" as const, type: "subprocess_import" },
{ pattern: /from\s+subprocess\s+import/, severity: "high" as const, type: "subprocess_import" },
{ pattern: /import\s+os/, severity: "high" as const, type: "os_import" },
{ pattern: /from\s+os\s+import/, severity: "high" as const, type: "os_import" },
{ pattern: /import\s+sys/, severity: "high" as const, type: "sys_import" },
{ pattern: /from\s+sys\s+import/, severity: "high" as const, type: "sys_import" },
{ pattern: /import\s+socket/, severity: "high" as const, type: "socket_import" },
{ pattern: /from\s+socket\s+import/, severity: "high" as const, type: "socket_import" },
{ pattern: /import\s+pickle/, severity: "high" as const, type: "pickle_import" },
{ pattern: /from\s+pickle\s+import/, severity: "high" as const, type: "pickle_import" },
// File operations
{ pattern: /\bopen\s*\(/, severity: "medium" as const, type: "file_open" },
{ pattern: /\bfile\s*\(/, severity: "medium" as const, type: "file_operation" },
{ pattern: /\.write\s*\(/, severity: "medium" as const, type: "file_write" },
{ pattern: /\.read\s*\(/, severity: "medium" as const, type: "file_read" },
// Network operations
{ pattern: /requests\.(get|post|put|delete|patch)/, severity: "medium" as const, type: "network_request" },
{ pattern: /urllib\.request/, severity: "medium" as const, type: "network_urllib" },
{ pattern: /http\.client/, severity: "medium" as const, type: "network_http" },
]
// Dangerous patterns in markdown cells (potential XSS)
const DANGEROUS_MARKDOWN_PATTERNS = [
{ pattern: /<script[^>]*>[\s\S]*?<\/script>/gi, severity: "high" as const, type: "script_tag" },
{ pattern: /<iframe[^>]*>/gi, severity: "high" as const, type: "iframe_tag" },
{ pattern: /javascript:/gi, severity: "high" as const, type: "javascript_protocol" },
{ pattern: /on\w+\s*=/gi, severity: "medium" as const, type: "event_handler" },
]
// Dangerous output patterns
const DANGEROUS_OUTPUT_PATTERNS = [
{ pattern: /<script[^>]*>[\s\S]*?<\/script>/gi, severity: "high" as const, type: "output_script" },
{ pattern: /data:text\/html/gi, severity: "medium" as const, type: "html_data_uri" },
]
export class JupyterNotebookSecurity {
private config: JupyterSecurityConfig
constructor(config?: JupyterSecurityConfig) {
this.config = { ...DEFAULT_CONFIG, ...config }
}
/**
* Validate a Jupyter notebook for security risks
*/
validateNotebook(notebook: JupyterNotebook, filePath?: string): SecurityValidationResult {
// If YOLO Mode is enabled, bypass all security checks
if (this.config.yoloMode) {
return {
isSecure: true,
risks: [],
requiresReadOnly: false,
}
}
const risks: SecurityRisk[] = []
// Check if source is trusted
if (filePath && this.config.trustedSources?.length) {
const isTrusted = this.config.trustedSources.some((source) => filePath.includes(source))
if (isTrusted) {
return {
isSecure: true,
risks: [],
requiresReadOnly: false,
}
}
}
// Check cell count
if (notebook.cells.length > (this.config.maxCellCount || DEFAULT_CONFIG.maxCellCount!)) {
risks.push({
severity: "medium",
type: "excessive_cells",
description: `Notebook has ${notebook.cells.length} cells, exceeding limit of ${this.config.maxCellCount}`,
})
}
// Validate each cell
notebook.cells.forEach((cell, index) => {
const cellRisks = this.validateCell(cell, index)
risks.push(...cellRisks)
})
// Check metadata for suspicious fields
if (notebook.metadata) {
const metadataRisks = this.validateMetadata(notebook.metadata)
risks.push(...metadataRisks)
}
// Determine security status
const hasCriticalRisk = risks.some((r) => r.severity === "critical")
const hasHighRisk = risks.some((r) => r.severity === "high")
const isSecure = !hasCriticalRisk && !hasHighRisk
// Only require read-only if explicitly configured OR if there are critical/high risks
const requiresReadOnly = this.config.readOnlyMode === true || hasCriticalRisk || hasHighRisk
// Optionally sanitize the notebook
let sanitizedNotebook: JupyterNotebook | undefined
if (!isSecure && this.config.allowCodeExecution === false) {
sanitizedNotebook = this.sanitizeNotebook(notebook, risks)
}
return {
isSecure,
risks,
requiresReadOnly,
sanitizedNotebook,
}
}
/**
* Validate a single cell for security risks
*/
private validateCell(cell: JupyterCell, index: number): SecurityRisk[] {
const risks: SecurityRisk[] = []
const source = Array.isArray(cell.source) ? cell.source.join("") : cell.source || ""
// Check cell size
const cellSize = new TextEncoder().encode(source).length
if (cellSize > (this.config.maxCellSize || DEFAULT_CONFIG.maxCellSize!)) {
risks.push({
severity: "medium",
type: "oversized_cell",
cellIndex: index,
cellType: cell.cell_type,
description: `Cell ${index} exceeds size limit (${cellSize} bytes)`,
})
}
// Check for dangerous patterns based on cell type
if (cell.cell_type === "code") {
// Check code patterns
for (const { pattern, severity, type } of DANGEROUS_CODE_PATTERNS) {
if (pattern.test(source)) {
risks.push({
severity,
type,
cellIndex: index,
cellType: "code",
description: `Dangerous code pattern detected: ${type}`,
pattern: pattern.toString(),
})
}
}
// Check outputs for dangerous content
if (cell.outputs && Array.isArray(cell.outputs)) {
for (const output of cell.outputs) {
if (output.data) {
const outputRisks = this.validateOutputData(output.data, index)
risks.push(...outputRisks)
}
}
}
} else if (cell.cell_type === "markdown") {
// Check markdown patterns
for (const { pattern, severity, type } of DANGEROUS_MARKDOWN_PATTERNS) {
if (pattern.test(source)) {
risks.push({
severity,
type,
cellIndex: index,
cellType: "markdown",
description: `Dangerous markdown pattern detected: ${type}`,
pattern: pattern.toString(),
})
}
}
}
return risks
}
/**
* Validate output data for security risks
*/
private validateOutputData(data: any, cellIndex: number): SecurityRisk[] {
const risks: SecurityRisk[] = []
// Check HTML outputs
if (data["text/html"]) {
const htmlContent = Array.isArray(data["text/html"]) ? data["text/html"].join("") : data["text/html"]
for (const { pattern, severity, type } of DANGEROUS_OUTPUT_PATTERNS) {
if (pattern.test(htmlContent)) {
risks.push({
severity,
type,
cellIndex,
cellType: "output",
description: `Dangerous output pattern detected: ${type}`,
pattern: pattern.toString(),
})
}
}
}
// Check JavaScript outputs
if (data["application/javascript"]) {
risks.push({
severity: "high",
type: "javascript_output",
cellIndex,
cellType: "output",
description: "JavaScript output detected",
})
}
return risks
}
/**
* Validate notebook metadata for security risks
*/
private validateMetadata(metadata: Record<string, any>): SecurityRisk[] {
const risks: SecurityRisk[] = []
// Check for suspicious metadata fields
const suspiciousFields = ["widgets", "extensions", "jupyter_dashboards"]
for (const field of suspiciousFields) {
if (metadata[field]) {
risks.push({
severity: "low",
type: "suspicious_metadata",
description: `Suspicious metadata field detected: ${field}`,
})
}
}
return risks
}
/**
* Sanitize a notebook by removing or disabling dangerous content
*/
private sanitizeNotebook(notebook: JupyterNotebook, risks: SecurityRisk[]): JupyterNotebook {
const sanitized = JSON.parse(JSON.stringify(notebook)) as JupyterNotebook
// Group risks by cell index
const risksByCell = new Map<number, SecurityRisk[]>()
for (const risk of risks) {
if (risk.cellIndex !== undefined) {
if (!risksByCell.has(risk.cellIndex)) {
risksByCell.set(risk.cellIndex, [])
}
risksByCell.get(risk.cellIndex)!.push(risk)
}
}
// Sanitize cells with risks
for (const [cellIndex, cellRisks] of risksByCell) {
const cell = sanitized.cells[cellIndex]
if (!cell) continue
const hasCriticalRisk = cellRisks.some((r) => r.severity === "critical")
const hasHighRisk = cellRisks.some((r) => r.severity === "high")
if (cell.cell_type === "code" && (hasCriticalRisk || hasHighRisk)) {
// Disable dangerous code cells
const riskTypes = cellRisks.map((r) => r.type).join(", ")
const warningComment = `# ⚠️ SECURITY WARNING: This cell has been disabled due to security risks (${riskTypes})\n# To run this cell, enable YOLO Mode in settings\n\n`
if (Array.isArray(cell.source)) {
const originalCode = cell.source.map((line) => `# ${line}`)
cell.source = [warningComment, "# Original code:\n", ...originalCode]
} else {
const originalCode = (cell.source || "")
.split("\n")
.map((line) => `# ${line}`)
.join("\n")
cell.source = warningComment + "# Original code:\n" + originalCode
}
// Clear outputs
cell.outputs = []
cell.execution_count = null
} else if (cell.cell_type === "markdown" && (hasCriticalRisk || hasHighRisk)) {
// Sanitize dangerous markdown
let source = Array.isArray(cell.source) ? cell.source.join("") : cell.source || ""
// Remove script tags
source = source.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "[REMOVED: script tag]")
// Remove iframes
source = source.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, "[REMOVED: iframe]")
// Remove javascript: protocols
source = source.replace(/javascript:/gi, "[REMOVED]:")
// Remove event handlers
source = source.replace(/on\w+\s*=/gi, "data-removed=")
// Handle both array and string source formats
if (Array.isArray(cell.source)) {
cell.source = source
.split("\n")
.map((line, idx, arr) => (idx === arr.length - 1 && line === "" ? line : line + "\n"))
} else {
cell.source = source
}
}
// Clear dangerous outputs
if (cell.outputs && Array.isArray(cell.outputs)) {
cell.outputs = cell.outputs.map((output) => {
if (output && output.data) {
const newOutput = { ...output, data: { ...output.data } }
// Remove HTML outputs with scripts
if (newOutput.data["text/html"]) {
const html = Array.isArray(newOutput.data["text/html"])
? newOutput.data["text/html"].join("")
: newOutput.data["text/html"]
if (/<script|<iframe|javascript:/i.test(html)) {
delete newOutput.data["text/html"]
// Only add text/plain if it doesn't exist
if (!newOutput.data["text/plain"]) {
newOutput.data["text/plain"] = ["[HTML output removed for security]"]
}
}
}
// Remove JavaScript outputs
if (newOutput.data["application/javascript"]) {
delete newOutput.data["application/javascript"]
// Add or append to text/plain
if (!newOutput.data["text/plain"]) {
newOutput.data["text/plain"] = ["[JavaScript output removed for security]"]
} else if (Array.isArray(newOutput.data["text/plain"])) {
newOutput.data["text/plain"].push("[JavaScript output removed for security]")
}
}
return newOutput
}
return output
})
}
}
// Remove suspicious metadata
if (sanitized.metadata) {
delete sanitized.metadata.widgets
delete sanitized.metadata.extensions
delete sanitized.metadata.jupyter_dashboards
}
return sanitized
}
/**
* Check if YOLO Mode is enabled
*/
isYoloModeEnabled(): boolean {
return this.config.yoloMode === true
}
/**
* Update security configuration
*/
updateConfig(config: Partial<JupyterSecurityConfig>): void {
this.config = { ...this.config, ...config }
}
}