diff --git a/docs/jupyter-notebook-security.md b/docs/jupyter-notebook-security.md
new file mode 100644
index 0000000000..c7f3e88b1e
--- /dev/null
+++ b/docs/jupyter-notebook-security.md
@@ -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**: `
+
+"""
+```
+
+### 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
diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts
index 81c6ae6dfe..492e5d6dc4 100644
--- a/packages/types/src/global-settings.ts
+++ b/packages/types/src/global-settings.ts
@@ -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
diff --git a/src/core/diff/strategies/jupyter-notebook-diff.ts b/src/core/diff/strategies/jupyter-notebook-diff.ts
index 031c230a63..059bf25c4f 100644
--- a/src/core/diff/strategies/jupyter-notebook-diff.ts
+++ b/src/core/diff/strategies/jupyter-notebook-diff.ts
@@ -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/,
diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts
index 615079da75..a49422ce5e 100644
--- a/src/core/task/Task.ts
+++ b/src/core/task/Task.ts
@@ -2705,17 +2705,23 @@ export class Task extends EventEmitter 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,
diff --git a/src/integrations/misc/__tests__/jupyter-notebook-security.spec.ts b/src/integrations/misc/__tests__/jupyter-notebook-security.spec.ts
new file mode 100644
index 0000000000..678cffc3a8
--- /dev/null
+++ b/src/integrations/misc/__tests__/jupyter-notebook-security.spec.ts
@@ -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",
+ 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: "",
+ 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": "",
+ },
+ },
+ ],
+ },
+ ],
+ }
+ 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: "",
+ 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\n",
+ 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("",
+ "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": "Safe HTML
",
+ "text/plain": ["safe output"],
+ },
+ },
+ {
+ data: {
+ "text/html": "",
+ "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()
+ })
+ })
+})
diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts
index 58d166153e..e10df22ef0 100644
--- a/src/integrations/misc/extract-text.ts
+++ b/src/integrations/misc/extract-text.ts
@@ -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 {
const dataBuffer = await fs.readFile(filePath)
@@ -20,9 +21,32 @@ async function extractTextFromDOCX(filePath: string): Promise {
return addLineNumbers(result.value)
}
-async function extractTextFromIPYNB(filePath: string): Promise {
- const handler = await JupyterNotebookHandler.fromFile(filePath)
- return handler.extractTextWithCellMarkers()
+async function extractTextFromIPYNB(filePath: string, securityConfig?: JupyterSecurityConfig): Promise {
+ 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
}
/**
diff --git a/src/integrations/misc/jupyter-notebook-handler.ts b/src/integrations/misc/jupyter-notebook-handler.ts
index 529dff5a38..a5f22dbc89 100644
--- a/src/integrations/misc/jupyter-notebook-handler.ts
+++ b/src/integrations/misc/jupyter-notebook-handler.ts
@@ -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 {
+ static async fromFile(filePath: string, securityConfig?: JupyterSecurityConfig): Promise {
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 {
+ // 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)
+ }
}
diff --git a/src/integrations/misc/jupyter-notebook-security.ts b/src/integrations/misc/jupyter-notebook-security.ts
new file mode 100644
index 0000000000..d48f0909bf
--- /dev/null
+++ b/src/integrations/misc/jupyter-notebook-security.ts
@@ -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: /