PyVaultRCE 2.4.0__tar.gz → 2.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/PKG-INFO +15 -7
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/PKG-INFO +15 -7
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/README.md +14 -6
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/codemanager/__init__.py +1 -1
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/codemanager/manager.py +106 -26
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/pyvaultrce/__init__.py +1 -1
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/pyvaultrce/manager.py +122 -28
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/setup.py +1 -1
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/SOURCES.txt +0 -0
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/dependency_links.txt +0 -0
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/requires.txt +0 -0
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/PyVaultRCE.egg-info/top_level.txt +0 -0
- {pyvaultrce-2.4.0 → pyvaultrce-2.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyVaultRCE
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Remote Code Execution & Hosting client for PyVault — source never exposed
|
|
5
5
|
Author: PyVault
|
|
6
6
|
Keywords: remote execution code hosting vault rce session
|
|
@@ -47,7 +47,7 @@ sid = CodeManager.enc("my_script.py")
|
|
|
47
47
|
# Upload from a paste service URL
|
|
48
48
|
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
49
49
|
|
|
50
|
-
# Execute
|
|
50
|
+
# Execute in a short-lived isolated subprocess by default
|
|
51
51
|
CodeManager.run(sid)
|
|
52
52
|
|
|
53
53
|
# Upload with a short share link and library metadata
|
|
@@ -55,6 +55,9 @@ sid = CodeManager.enc(
|
|
|
55
55
|
"my_script.py",
|
|
56
56
|
label="analytics demo",
|
|
57
57
|
libraries=["requests", "pandas"],
|
|
58
|
+
username="your_name",
|
|
59
|
+
description="A public demo post",
|
|
60
|
+
tags=["python", "demo"],
|
|
58
61
|
)
|
|
59
62
|
|
|
60
63
|
# Hide PyVault status messages while running
|
|
@@ -72,11 +75,12 @@ CodeManager.ping()
|
|
|
72
75
|
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
|
-
export PYVAULT_URL=https://
|
|
78
|
+
export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
|
|
82
|
+
For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
|
|
83
|
+
You can also control status output globally:
|
|
80
84
|
|
|
81
85
|
```bash
|
|
82
86
|
export PYVAULT_TERMINAL=off
|
|
@@ -91,9 +95,9 @@ where `CodeManager.run()` executes.
|
|
|
91
95
|
| Method | Description |
|
|
92
96
|
|--------|-------------|
|
|
93
97
|
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
94
|
-
| `CodeManager.enc(file_path, label=..., libraries=[...])` | Upload with
|
|
98
|
+
| `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
|
|
95
99
|
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
96
|
-
| `CodeManager.run(session_id, show_terminal=False)` | Fetch and execute code
|
|
100
|
+
| `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
|
|
97
101
|
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
98
102
|
| `CodeManager.ping()` | Check server connectivity |
|
|
99
103
|
| `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
|
|
@@ -124,6 +128,10 @@ where `CodeManager.run()` executes.
|
|
|
124
128
|
- Session IDs are 21-character cryptographically random hex strings
|
|
125
129
|
- Source code is never written to disk on the client
|
|
126
130
|
- Code is encrypted at rest with Fernet on the server
|
|
131
|
+
- Code runs in a short-lived subprocess by default with a clean environment,
|
|
132
|
+
temporary working directory, CPU/memory/file-size limits, and a hard timeout
|
|
133
|
+
- `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
|
|
134
|
+
it is not recommended for untrusted sessions
|
|
127
135
|
- Share links expose only session metadata and declared library names, never source
|
|
128
136
|
- Admin edits require the admin access code; owner edits/deletes require the one-time owner token
|
|
129
137
|
- Sessions never expire unless an expiry is explicitly selected
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyVaultRCE
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: Remote Code Execution & Hosting client for PyVault — source never exposed
|
|
5
5
|
Author: PyVault
|
|
6
6
|
Keywords: remote execution code hosting vault rce session
|
|
@@ -47,7 +47,7 @@ sid = CodeManager.enc("my_script.py")
|
|
|
47
47
|
# Upload from a paste service URL
|
|
48
48
|
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
49
49
|
|
|
50
|
-
# Execute
|
|
50
|
+
# Execute in a short-lived isolated subprocess by default
|
|
51
51
|
CodeManager.run(sid)
|
|
52
52
|
|
|
53
53
|
# Upload with a short share link and library metadata
|
|
@@ -55,6 +55,9 @@ sid = CodeManager.enc(
|
|
|
55
55
|
"my_script.py",
|
|
56
56
|
label="analytics demo",
|
|
57
57
|
libraries=["requests", "pandas"],
|
|
58
|
+
username="your_name",
|
|
59
|
+
description="A public demo post",
|
|
60
|
+
tags=["python", "demo"],
|
|
58
61
|
)
|
|
59
62
|
|
|
60
63
|
# Hide PyVault status messages while running
|
|
@@ -72,11 +75,12 @@ CodeManager.ping()
|
|
|
72
75
|
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
|
-
export PYVAULT_URL=https://
|
|
78
|
+
export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
|
|
82
|
+
For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
|
|
83
|
+
You can also control status output globally:
|
|
80
84
|
|
|
81
85
|
```bash
|
|
82
86
|
export PYVAULT_TERMINAL=off
|
|
@@ -91,9 +95,9 @@ where `CodeManager.run()` executes.
|
|
|
91
95
|
| Method | Description |
|
|
92
96
|
|--------|-------------|
|
|
93
97
|
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
94
|
-
| `CodeManager.enc(file_path, label=..., libraries=[...])` | Upload with
|
|
98
|
+
| `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
|
|
95
99
|
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
96
|
-
| `CodeManager.run(session_id, show_terminal=False)` | Fetch and execute code
|
|
100
|
+
| `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
|
|
97
101
|
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
98
102
|
| `CodeManager.ping()` | Check server connectivity |
|
|
99
103
|
| `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
|
|
@@ -124,6 +128,10 @@ where `CodeManager.run()` executes.
|
|
|
124
128
|
- Session IDs are 21-character cryptographically random hex strings
|
|
125
129
|
- Source code is never written to disk on the client
|
|
126
130
|
- Code is encrypted at rest with Fernet on the server
|
|
131
|
+
- Code runs in a short-lived subprocess by default with a clean environment,
|
|
132
|
+
temporary working directory, CPU/memory/file-size limits, and a hard timeout
|
|
133
|
+
- `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
|
|
134
|
+
it is not recommended for untrusted sessions
|
|
127
135
|
- Share links expose only session metadata and declared library names, never source
|
|
128
136
|
- Admin edits require the admin access code; owner edits/deletes require the one-time owner token
|
|
129
137
|
- Sessions never expire unless an expiry is explicitly selected
|
|
@@ -22,7 +22,7 @@ sid = CodeManager.enc("my_script.py")
|
|
|
22
22
|
# Upload from a paste service URL
|
|
23
23
|
sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
|
|
24
24
|
|
|
25
|
-
# Execute
|
|
25
|
+
# Execute in a short-lived isolated subprocess by default
|
|
26
26
|
CodeManager.run(sid)
|
|
27
27
|
|
|
28
28
|
# Upload with a short share link and library metadata
|
|
@@ -30,6 +30,9 @@ sid = CodeManager.enc(
|
|
|
30
30
|
"my_script.py",
|
|
31
31
|
label="analytics demo",
|
|
32
32
|
libraries=["requests", "pandas"],
|
|
33
|
+
username="your_name",
|
|
34
|
+
description="A public demo post",
|
|
35
|
+
tags=["python", "demo"],
|
|
33
36
|
)
|
|
34
37
|
|
|
35
38
|
# Hide PyVault status messages while running
|
|
@@ -47,11 +50,12 @@ CodeManager.ping()
|
|
|
47
50
|
Set `PYVAULT_URL` to point at your hosted PyVault server:
|
|
48
51
|
|
|
49
52
|
```bash
|
|
50
|
-
export PYVAULT_URL=https://
|
|
53
|
+
export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
|
|
57
|
+
For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
|
|
58
|
+
You can also control status output globally:
|
|
55
59
|
|
|
56
60
|
```bash
|
|
57
61
|
export PYVAULT_TERMINAL=off
|
|
@@ -66,9 +70,9 @@ where `CodeManager.run()` executes.
|
|
|
66
70
|
| Method | Description |
|
|
67
71
|
|--------|-------------|
|
|
68
72
|
| `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
|
|
69
|
-
| `CodeManager.enc(file_path, label=..., libraries=[...])` | Upload with
|
|
73
|
+
| `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
|
|
70
74
|
| `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
|
|
71
|
-
| `CodeManager.run(session_id, show_terminal=False)` | Fetch and execute code
|
|
75
|
+
| `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
|
|
72
76
|
| `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
|
|
73
77
|
| `CodeManager.ping()` | Check server connectivity |
|
|
74
78
|
| `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
|
|
@@ -99,6 +103,10 @@ where `CodeManager.run()` executes.
|
|
|
99
103
|
- Session IDs are 21-character cryptographically random hex strings
|
|
100
104
|
- Source code is never written to disk on the client
|
|
101
105
|
- Code is encrypted at rest with Fernet on the server
|
|
106
|
+
- Code runs in a short-lived subprocess by default with a clean environment,
|
|
107
|
+
temporary working directory, CPU/memory/file-size limits, and a hard timeout
|
|
108
|
+
- `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
|
|
109
|
+
it is not recommended for untrusted sessions
|
|
102
110
|
- Share links expose only session metadata and declared library names, never source
|
|
103
111
|
- Admin edits require the admin access code; owner edits/deletes require the one-time owner token
|
|
104
112
|
- Sessions never expire unless an expiry is explicitly selected
|
|
@@ -3,7 +3,9 @@ manager.py — CodeManager implementation
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import subprocess
|
|
6
7
|
import sys
|
|
8
|
+
import tempfile
|
|
7
9
|
import traceback
|
|
8
10
|
import textwrap
|
|
9
11
|
from typing import Optional
|
|
@@ -15,7 +17,7 @@ except ImportError:
|
|
|
15
17
|
"The 'requests' library is required. Install it with: pip install requests"
|
|
16
18
|
)
|
|
17
19
|
|
|
18
|
-
_DEFAULT_BASE_URL = "
|
|
20
|
+
_DEFAULT_BASE_URL = "https://secure-code-runner--diwasrepl.replit.app"
|
|
19
21
|
|
|
20
22
|
# ── Validation helpers ───────────────────────────────────────────────────────
|
|
21
23
|
|
|
@@ -53,6 +55,57 @@ def _emit(message: str, show_terminal=None, *, error=False) -> None:
|
|
|
53
55
|
print(message, file=sys.stderr if error else sys.stdout, flush=True)
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
def _isolated_run(code: str, session_id: str, timeout: int) -> subprocess.CompletedProcess:
|
|
59
|
+
"""Execute in a short-lived process with clean environment and limits."""
|
|
60
|
+
def limit_resources():
|
|
61
|
+
if os.name != "posix":
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
import resource
|
|
65
|
+
cpu = max(1, min(int(timeout), 30))
|
|
66
|
+
# Python's runtime reserves virtual address space during startup;
|
|
67
|
+
# 512 MiB breaks otherwise tiny scripts on some Linux builds.
|
|
68
|
+
memory = 2 * 1024 * 1024 * 1024
|
|
69
|
+
resource.setrlimit(resource.RLIMIT_CPU, (cpu, cpu))
|
|
70
|
+
resource.setrlimit(resource.RLIMIT_AS, (memory, memory))
|
|
71
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024))
|
|
72
|
+
except (ImportError, OSError, ValueError):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
clean_env = {
|
|
76
|
+
key: os.environ[key]
|
|
77
|
+
for key in ("PATH", "SystemRoot", "SYSTEMROOT", "TEMP", "TMP", "HOME", "LANG", "LC_ALL")
|
|
78
|
+
if key in os.environ
|
|
79
|
+
}
|
|
80
|
+
clean_env.update({"PYTHONNOUSERSITE": "1", "PYTHONUNBUFFERED": "1"})
|
|
81
|
+
with tempfile.TemporaryDirectory(prefix="pyvault-run-") as workdir:
|
|
82
|
+
script_path = os.path.join(workdir, "session.py")
|
|
83
|
+
with open(script_path, "w", encoding="utf-8") as handle:
|
|
84
|
+
handle.write(code)
|
|
85
|
+
try:
|
|
86
|
+
return subprocess.run(
|
|
87
|
+
[sys.executable, "-I", "-u", script_path],
|
|
88
|
+
cwd=workdir,
|
|
89
|
+
env=clean_env,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
timeout=max(1, int(timeout)),
|
|
93
|
+
check=False,
|
|
94
|
+
preexec_fn=limit_resources if os.name == "posix" else None,
|
|
95
|
+
)
|
|
96
|
+
except subprocess.TimeoutExpired as exc:
|
|
97
|
+
raise TimeoutError(
|
|
98
|
+
f"Isolated session '{session_id}' exceeded the {timeout}s execution timeout."
|
|
99
|
+
) from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _emit_child_output(result: subprocess.CompletedProcess) -> None:
|
|
103
|
+
if result.stdout:
|
|
104
|
+
print(result.stdout, end="", flush=True)
|
|
105
|
+
if result.stderr:
|
|
106
|
+
print(result.stderr, end="", file=sys.stderr, flush=True)
|
|
107
|
+
|
|
108
|
+
|
|
56
109
|
# ── CodeManager ─────────────────────────────────────────────────────────────
|
|
57
110
|
|
|
58
111
|
class CodeManager:
|
|
@@ -74,6 +127,9 @@ class CodeManager:
|
|
|
74
127
|
timeout: int = 30,
|
|
75
128
|
label: str = "",
|
|
76
129
|
libraries = "",
|
|
130
|
+
username: str = "",
|
|
131
|
+
description: str = "",
|
|
132
|
+
tags = "",
|
|
77
133
|
) -> str:
|
|
78
134
|
"""
|
|
79
135
|
Read a local Python file and upload it to the PyVault backend.
|
|
@@ -113,7 +169,14 @@ class CodeManager:
|
|
|
113
169
|
try:
|
|
114
170
|
response = requests.post(
|
|
115
171
|
url,
|
|
116
|
-
json={
|
|
172
|
+
json={
|
|
173
|
+
"code": code,
|
|
174
|
+
"label": label,
|
|
175
|
+
"libraries": libraries,
|
|
176
|
+
"username": username,
|
|
177
|
+
"description": description,
|
|
178
|
+
"tags": tags,
|
|
179
|
+
},
|
|
117
180
|
timeout=timeout,
|
|
118
181
|
)
|
|
119
182
|
except requests.exceptions.ConnectionError:
|
|
@@ -158,12 +221,16 @@ class CodeManager:
|
|
|
158
221
|
timeout: int = 30,
|
|
159
222
|
globals_dict: Optional[dict] = None,
|
|
160
223
|
show_terminal=None,
|
|
224
|
+
isolated: bool = True,
|
|
161
225
|
) -> None:
|
|
162
226
|
"""
|
|
163
|
-
Fetch the code for a Session ID from PyVault and execute it
|
|
227
|
+
Fetch the code for a Session ID from PyVault and execute it in a
|
|
228
|
+
short-lived isolated subprocess by default.
|
|
164
229
|
|
|
165
|
-
The source code is fetched over the network
|
|
166
|
-
|
|
230
|
+
The source code is fetched over the network. The default subprocess
|
|
231
|
+
uses a clean environment, a temporary working directory, and resource
|
|
232
|
+
limits. Use isolated=False only for trusted code that needs a shared
|
|
233
|
+
in-process namespace.
|
|
167
234
|
|
|
168
235
|
Args:
|
|
169
236
|
session_id: The 21-character hex Session ID.
|
|
@@ -224,28 +291,41 @@ class CodeManager:
|
|
|
224
291
|
f"Server returned empty code for session '{session_id}'."
|
|
225
292
|
)
|
|
226
293
|
|
|
227
|
-
_emit(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
294
|
+
_emit(
|
|
295
|
+
f"[PyVault] Executing session '{session_id}' "
|
|
296
|
+
f"(run #{exec_count}, isolated={isolated})…",
|
|
297
|
+
show_terminal,
|
|
298
|
+
)
|
|
233
299
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
300
|
+
if isolated:
|
|
301
|
+
if globals_dict is not None:
|
|
302
|
+
raise ValueError("isolated=True cannot use globals_dict; pass isolated=False for a shared namespace.")
|
|
303
|
+
result = _isolated_run(code, session_id, timeout)
|
|
304
|
+
_emit_child_output(result)
|
|
305
|
+
if result.returncode != 0:
|
|
306
|
+
raise RuntimeError(
|
|
307
|
+
f"Isolated session '{session_id}' exited with code {result.returncode}."
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
namespace = globals_dict if globals_dict is not None else {
|
|
311
|
+
"__name__": "__pyvault__",
|
|
312
|
+
"__builtins__": __builtins__,
|
|
313
|
+
}
|
|
314
|
+
try:
|
|
315
|
+
compiled = compile(code, f"<pyvault:{session_id}>", "exec")
|
|
316
|
+
exec(compiled, namespace)
|
|
317
|
+
except SyntaxError as exc:
|
|
318
|
+
_emit("\n[PyVault] ✕ Syntax error in remote code:", show_terminal, error=True)
|
|
319
|
+
_emit(f" Line {exc.lineno}: {exc.msg}", show_terminal, error=True)
|
|
320
|
+
if exc.text:
|
|
321
|
+
_emit(f" >>> {exc.text.strip()}", show_terminal, error=True)
|
|
322
|
+
raise
|
|
323
|
+
except Exception:
|
|
324
|
+
_emit("\n[PyVault] ✕ Runtime error during execution:", show_terminal, error=True)
|
|
325
|
+
tb_lines = traceback.format_exc().splitlines()
|
|
326
|
+
for line in tb_lines:
|
|
327
|
+
_emit(f" {line}", show_terminal, error=True)
|
|
328
|
+
raise
|
|
249
329
|
|
|
250
330
|
_emit("[PyVault] ✓ Execution complete.", show_terminal)
|
|
251
331
|
|
|
@@ -4,7 +4,9 @@ manager.py — CodeManager implementation for PyVaultRCE
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
+
import subprocess
|
|
7
8
|
import sys
|
|
9
|
+
import tempfile
|
|
8
10
|
import traceback
|
|
9
11
|
from typing import Optional
|
|
10
12
|
|
|
@@ -23,7 +25,7 @@ try:
|
|
|
23
25
|
except ImportError:
|
|
24
26
|
_HAS_CRYPTO = False
|
|
25
27
|
|
|
26
|
-
_DEFAULT_BASE = "
|
|
28
|
+
_DEFAULT_BASE = "https://secure-code-runner--diwasrepl.replit.app"
|
|
27
29
|
_MAX_LINES = 10_000
|
|
28
30
|
_SID_LEN = 21
|
|
29
31
|
_HEX_SET = frozenset("0123456789abcdef")
|
|
@@ -171,6 +173,62 @@ def _localhost_hint(url: str) -> str:
|
|
|
171
173
|
return ""
|
|
172
174
|
|
|
173
175
|
|
|
176
|
+
def _isolated_run(code: str, session_id: str, timeout: int) -> subprocess.CompletedProcess:
|
|
177
|
+
"""Run fetched code in a short-lived process with a clean environment.
|
|
178
|
+
|
|
179
|
+
This is a defense-in-depth boundary for the client. It is intentionally
|
|
180
|
+
not described as a container or a complete OS sandbox: callers who need
|
|
181
|
+
hostile-code isolation should use a dedicated VM/container policy.
|
|
182
|
+
"""
|
|
183
|
+
def limit_resources():
|
|
184
|
+
if os.name != "posix":
|
|
185
|
+
return
|
|
186
|
+
try:
|
|
187
|
+
import resource
|
|
188
|
+
cpu = max(1, min(int(timeout), 30))
|
|
189
|
+
# Python's runtime reserves virtual address space during startup;
|
|
190
|
+
# 512 MiB breaks otherwise tiny scripts on some Linux builds.
|
|
191
|
+
memory = 2 * 1024 * 1024 * 1024
|
|
192
|
+
resource.setrlimit(resource.RLIMIT_CPU, (cpu, cpu))
|
|
193
|
+
resource.setrlimit(resource.RLIMIT_AS, (memory, memory))
|
|
194
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024))
|
|
195
|
+
except (ImportError, OSError, ValueError):
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
clean_env = {}
|
|
199
|
+
for key in ("PATH", "SystemRoot", "SYSTEMROOT", "TEMP", "TMP", "HOME", "LANG", "LC_ALL"):
|
|
200
|
+
if key in os.environ:
|
|
201
|
+
clean_env[key] = os.environ[key]
|
|
202
|
+
clean_env.update({"PYTHONNOUSERSITE": "1", "PYTHONUNBUFFERED": "1"})
|
|
203
|
+
|
|
204
|
+
with tempfile.TemporaryDirectory(prefix="pyvault-run-") as workdir:
|
|
205
|
+
script_path = os.path.join(workdir, "session.py")
|
|
206
|
+
with open(script_path, "w", encoding="utf-8") as handle:
|
|
207
|
+
handle.write(code)
|
|
208
|
+
try:
|
|
209
|
+
return subprocess.run(
|
|
210
|
+
[sys.executable, "-I", "-u", script_path],
|
|
211
|
+
cwd=workdir,
|
|
212
|
+
env=clean_env,
|
|
213
|
+
capture_output=True,
|
|
214
|
+
text=True,
|
|
215
|
+
timeout=max(1, int(timeout)),
|
|
216
|
+
check=False,
|
|
217
|
+
preexec_fn=limit_resources if os.name == "posix" else None,
|
|
218
|
+
)
|
|
219
|
+
except subprocess.TimeoutExpired as exc:
|
|
220
|
+
raise TimeoutError(
|
|
221
|
+
f"Isolated session '{session_id}' exceeded the {timeout}s execution timeout."
|
|
222
|
+
) from exc
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _emit_child_output(result: subprocess.CompletedProcess) -> None:
|
|
226
|
+
if result.stdout:
|
|
227
|
+
print(result.stdout, end="", flush=True)
|
|
228
|
+
if result.stderr:
|
|
229
|
+
print(result.stderr, end="", file=sys.stderr, flush=True)
|
|
230
|
+
|
|
231
|
+
|
|
174
232
|
def _upload_code(
|
|
175
233
|
code: str,
|
|
176
234
|
base_url: Optional[str],
|
|
@@ -179,6 +237,9 @@ def _upload_code(
|
|
|
179
237
|
max_executions: int = 0,
|
|
180
238
|
label: str = "",
|
|
181
239
|
libraries = "",
|
|
240
|
+
username: str = "",
|
|
241
|
+
description: str = "",
|
|
242
|
+
tags = "",
|
|
182
243
|
) -> tuple:
|
|
183
244
|
"""Internal: push code string to /pyv/save. Returns session metadata."""
|
|
184
245
|
if not code.strip():
|
|
@@ -197,6 +258,12 @@ def _upload_code(
|
|
|
197
258
|
payload["label"] = str(label)[:200]
|
|
198
259
|
if libraries:
|
|
199
260
|
payload["libraries"] = _normalise_libraries(libraries)
|
|
261
|
+
if username:
|
|
262
|
+
payload["username"] = str(username)[:24]
|
|
263
|
+
if description:
|
|
264
|
+
payload["description"] = str(description)[:1000]
|
|
265
|
+
if tags:
|
|
266
|
+
payload["tags"] = tags
|
|
200
267
|
|
|
201
268
|
url = _base(base_url) + "/pyv/save"
|
|
202
269
|
resp = _http_post(url, payload, timeout)
|
|
@@ -230,7 +297,7 @@ class CodeManager:
|
|
|
230
297
|
|
|
231
298
|
Environment variables:
|
|
232
299
|
PYVAULT_URL — base URL of the PyVault server
|
|
233
|
-
(default:
|
|
300
|
+
(default: the PyVault public deployment; set PYVAULT_URL to override)
|
|
234
301
|
PYVAULT_TERMINAL — on/off terminal status messages (default: on)
|
|
235
302
|
"""
|
|
236
303
|
|
|
@@ -243,6 +310,9 @@ class CodeManager:
|
|
|
243
310
|
max_executions: int = 0,
|
|
244
311
|
label: str = "",
|
|
245
312
|
libraries = "",
|
|
313
|
+
username: str = "",
|
|
314
|
+
description: str = "",
|
|
315
|
+
tags = "",
|
|
246
316
|
show_terminal: Optional[bool] = None,
|
|
247
317
|
) -> str:
|
|
248
318
|
"""
|
|
@@ -265,7 +335,8 @@ class CodeManager:
|
|
|
265
335
|
if not code.strip():
|
|
266
336
|
raise ValueError(f"The file '{file_path}' is empty — nothing to upload.")
|
|
267
337
|
sid, owner_token, share_url, safe_libraries = _upload_code(
|
|
268
|
-
code, base_url, timeout, expires_in_hours, max_executions, label, libraries
|
|
338
|
+
code, base_url, timeout, expires_in_hours, max_executions, label, libraries,
|
|
339
|
+
username, description, tags
|
|
269
340
|
)
|
|
270
341
|
_emit(f"[PyVault] ✓ Uploaded — Session ID : {sid}", show_terminal)
|
|
271
342
|
_emit(f"[PyVault] Source : {os.path.abspath(file_path)}", show_terminal)
|
|
@@ -287,6 +358,9 @@ class CodeManager:
|
|
|
287
358
|
timeout: int = 30,
|
|
288
359
|
label: str = "",
|
|
289
360
|
libraries = "",
|
|
361
|
+
username: str = "",
|
|
362
|
+
description: str = "",
|
|
363
|
+
tags = "",
|
|
290
364
|
show_terminal: Optional[bool] = None,
|
|
291
365
|
) -> str:
|
|
292
366
|
"""
|
|
@@ -311,7 +385,7 @@ class CodeManager:
|
|
|
311
385
|
resp = requests.get(
|
|
312
386
|
raw_url,
|
|
313
387
|
timeout=timeout,
|
|
314
|
-
headers={"User-Agent": "PyVaultRCE/2.
|
|
388
|
+
headers={"User-Agent": "PyVaultRCE/2.5"},
|
|
315
389
|
)
|
|
316
390
|
resp.raise_for_status()
|
|
317
391
|
except requests.exceptions.ConnectionError:
|
|
@@ -330,7 +404,8 @@ class CodeManager:
|
|
|
330
404
|
raise ValueError("Downloaded content is empty.")
|
|
331
405
|
|
|
332
406
|
sid, owner_token, share_url, safe_libraries = _upload_code(
|
|
333
|
-
code, base_url, timeout, label=label, libraries=libraries
|
|
407
|
+
code, base_url, timeout, label=label, libraries=libraries,
|
|
408
|
+
username=username, description=description, tags=tags
|
|
334
409
|
)
|
|
335
410
|
_emit(f"[PyVault] ✓ Uploaded from URL — Session ID : {sid}", show_terminal)
|
|
336
411
|
_emit(f"[PyVault] Source : {paste_url}", show_terminal)
|
|
@@ -352,20 +427,26 @@ class CodeManager:
|
|
|
352
427
|
timeout: int = 30,
|
|
353
428
|
_ns: Optional[dict] = None,
|
|
354
429
|
show_terminal: Optional[bool] = None,
|
|
430
|
+
isolated: bool = True,
|
|
355
431
|
) -> None:
|
|
356
432
|
"""
|
|
357
433
|
Fetch the encrypted code for a Session ID from PyVault, decrypt it,
|
|
358
|
-
and execute it
|
|
434
|
+
and execute it in a short-lived isolated subprocess by default.
|
|
359
435
|
|
|
360
|
-
The source code is transmitted encrypted
|
|
361
|
-
|
|
436
|
+
The source code is transmitted encrypted and decrypted in memory. The
|
|
437
|
+
default subprocess writes it only to a temporary file for the child
|
|
438
|
+
process, then removes that directory after execution.
|
|
362
439
|
|
|
363
440
|
Args:
|
|
364
441
|
session_id: The 21-character hex Session ID.
|
|
365
442
|
base_url: Override the server URL.
|
|
366
443
|
timeout: HTTP timeout in seconds.
|
|
367
444
|
_ns: Optional namespace dict passed to exec(). Leave as None
|
|
368
|
-
|
|
445
|
+
unless you intentionally want to share a namespace;
|
|
446
|
+
providing it requires isolated=False.
|
|
447
|
+
isolated: Use the short-lived subprocess boundary (default True).
|
|
448
|
+
Set False only for trusted code that needs an in-process
|
|
449
|
+
namespace.
|
|
369
450
|
|
|
370
451
|
Raises:
|
|
371
452
|
ValueError, ConnectionError, RuntimeError, plus any exception
|
|
@@ -416,26 +497,39 @@ class CodeManager:
|
|
|
416
497
|
if not code.strip():
|
|
417
498
|
raise RuntimeError(f"Decrypted code is empty for session '{session_id}'.")
|
|
418
499
|
|
|
419
|
-
_emit(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
500
|
+
_emit(
|
|
501
|
+
f"[PyVault] ▶ Running session '{session_id}' "
|
|
502
|
+
f"(execution #{run_count}, isolated={isolated})…",
|
|
503
|
+
show_terminal,
|
|
504
|
+
)
|
|
425
505
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
506
|
+
if isolated:
|
|
507
|
+
if _ns is not None:
|
|
508
|
+
raise ValueError("isolated=True cannot use _ns; pass isolated=False for a shared namespace.")
|
|
509
|
+
result = _isolated_run(code, session_id, timeout)
|
|
510
|
+
_emit_child_output(result)
|
|
511
|
+
if result.returncode != 0:
|
|
512
|
+
raise RuntimeError(
|
|
513
|
+
f"Isolated session '{session_id}' exited with code {result.returncode}."
|
|
514
|
+
)
|
|
515
|
+
else:
|
|
516
|
+
namespace = _ns if _ns is not None else {
|
|
517
|
+
"__name__": "__pyvault__",
|
|
518
|
+
"__builtins__": __builtins__,
|
|
519
|
+
}
|
|
520
|
+
try:
|
|
521
|
+
exec(compile(code, f"<vault:{session_id[:8]}…>", "exec"), namespace)
|
|
522
|
+
except SyntaxError as exc:
|
|
523
|
+
_emit("\n[PyVault] ✕ Syntax error:", show_terminal, error=True)
|
|
524
|
+
_emit(f" Line {exc.lineno}: {exc.msg}", show_terminal, error=True)
|
|
525
|
+
if exc.text:
|
|
526
|
+
_emit(f" >>> {exc.text.strip()}", show_terminal, error=True)
|
|
527
|
+
raise
|
|
528
|
+
except Exception:
|
|
529
|
+
_emit("\n[PyVault] ✕ Runtime error:", show_terminal, error=True)
|
|
530
|
+
for line in traceback.format_exc().splitlines():
|
|
531
|
+
_emit(f" {line}", show_terminal, error=True)
|
|
532
|
+
raise
|
|
439
533
|
|
|
440
534
|
_emit("[PyVault] ✓ Execution complete.", show_terminal)
|
|
441
535
|
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="PyVaultRCE",
|
|
8
|
-
version="2.
|
|
8
|
+
version="2.5.0",
|
|
9
9
|
author="PyVault",
|
|
10
10
|
description="Remote Code Execution & Hosting client for PyVault — source never exposed",
|
|
11
11
|
long_description=long_description,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|