Translated ['src/network-services-pentesting/pentesting-web/nextjs-1.md'

This commit is contained in:
Translator 2025-04-30 12:03:17 +00:00
parent 6ead1a6db0
commit 77fa73a16c
4 changed files with 428 additions and 114 deletions

View File

@ -393,8 +393,6 @@
- [Electron contextIsolation RCE via Electron internal code](network-services-pentesting/pentesting-web/electron-desktop-apps/electron-contextisolation-rce-via-electron-internal-code.md)
- [Electron contextIsolation RCE via IPC](network-services-pentesting/pentesting-web/electron-desktop-apps/electron-contextisolation-rce-via-ipc.md)
- [Flask](network-services-pentesting/pentesting-web/flask.md)
- [NextJS](network-services-pentesting/pentesting-web/nextjs.md)
- [NodeJS Express](network-services-pentesting/pentesting-web/nodejs-express.md)
- [Git](network-services-pentesting/pentesting-web/git.md)
- [Golang](network-services-pentesting/pentesting-web/golang.md)
- [GWT - Google Web Toolkit](network-services-pentesting/pentesting-web/gwt-google-web-toolkit.md)
@ -409,8 +407,9 @@
- [JSP](network-services-pentesting/pentesting-web/jsp.md)
- [Laravel](network-services-pentesting/pentesting-web/laravel.md)
- [Moodle](network-services-pentesting/pentesting-web/moodle.md)
- [NextJS](network-services-pentesting/pentesting-web/nextjs.md)
- [Nginx](network-services-pentesting/pentesting-web/nginx.md)
- [NextJS](network-services-pentesting/pentesting-web/nextjs-1.md)
- [NodeJS Express](network-services-pentesting/pentesting-web/nodejs-express.md)
- [PHP Tricks](network-services-pentesting/pentesting-web/php-tricks-esp/README.md)
- [PHP - Useful Functions & disable_functions/open_basedir bypass](network-services-pentesting/pentesting-web/php-tricks-esp/php-useful-functions-disable_functions-open_basedir-bypass/README.md)
- [disable_functions bypass - php-fpm/FastCGI](network-services-pentesting/pentesting-web/php-tricks-esp/php-useful-functions-disable_functions-open_basedir-bypass/disable_functions-bypass-php-fpm-fastcgi.md)
@ -439,6 +438,7 @@
- [Symfony](network-services-pentesting/pentesting-web/symphony.md)
- [Tomcat](network-services-pentesting/pentesting-web/tomcat/README.md)
- [Uncovering CloudFlare](network-services-pentesting/pentesting-web/uncovering-cloudflare.md)
- [Vuejs](network-services-pentesting/pentesting-web/vuejs.md)
- [VMWare (ESX, VCenter...)](network-services-pentesting/pentesting-web/vmware-esx-vcenter....md)
- [Web API Pentesting](network-services-pentesting/pentesting-web/web-api-pentesting.md)
- [WebDav](network-services-pentesting/pentesting-web/put-method-webdav.md)

View File

@ -1,5 +0,0 @@
# NextJS
{{#include ../../banners/hacktricks-training.md}}
{{#include ../../banners/hacktricks-training.md}}

View File

@ -0,0 +1,128 @@
# Vue.js
{{#include ../../banners/hacktricks-training.md}}
## Vue.js में XSS Sinks
### v-html निर्देश
`v-html` निर्देश **कच्चा** HTML प्रस्तुत करता है, इसलिए कोई भी `<script>` (या `onerror` जैसी कोई विशेषता) जो अस्वच्छ उपयोगकर्ता इनपुट में निहित है, तुरंत निष्पादित हो जाती है।
```html
<div id="app">
<div v-html="htmlContent"></div>
</div>
<script>
new Vue({
el: '#app',
data: {
htmlContent: '<img src=x onerror=alert(1)>'
}
})
</script>
```
### v-bind with src or href
एक उपयोगकर्ता स्ट्रिंग को URL-bearing attributes (`href`, `src`, `xlink:href`, `formaction` …) से बाइंड करना payloads जैसे कि `javascript:alert(1)` को चलाने की अनुमति देता है जब लिंक का पालन किया जाता है।
```html
<div id="app">
<a v-bind:href="userInput">Click me</a>
</div>
<script>
new Vue({
el: '#app',
data: {
userInput: 'javascript:alert(1)'
}
})
</script>
```
### v-on के साथ उपयोगकर्ता-नियंत्रित हैंडलर्स
`v-on` अपने मान को `new Function` के साथ संकलित करता है; यदि वह मान उपयोगकर्ता से आता है, तो आप उन्हें कोड-निष्पादन एक प्लेट पर देते हैं।
```html
<div id="app">
<button v-on:click="malicious">Click me</button>
</div>
<script>
new Vue({
el: '#app',
data: { malicious: 'alert(1)' }
})
</script>
```
### Dynamic attribute / event names
उपयोगकर्ता द्वारा प्रदान किए गए नाम `v-bind:[attr]` या `v-on:[event]` में हमलावरों को किसी भी विशेषता या इवेंट हैंडलर को बनाने की अनुमति देते हैं, स्थिर विश्लेषण और कई CSP नियमों को बायपास करते हुए।
```html
<img v-bind:[userAttr]="payload">
<!-- userAttr = 'onerror', payload = 'alert(1)' -->
```
### Dynamic component (`<component :is>`)
`:is` में उपयोगकर्ता स्ट्रिंग्स की अनुमति देना मनमाने घटकों या इनलाइन टेम्पलेट्स को माउंट कर सकता है—ब्राउज़र में खतरनाक और SSR में विनाशकारी।
```html
<component :is="userChoice"></component>
<!-- userChoice = '<script>alert(1)</script>' -->
```
### Untrusted templates in SSR
सर्वर-साइड रेंडरिंग के दौरान, टेम्पलेट **आपके सर्वर पर** चलता है; उपयोगकर्ता HTML को इंजेक्ट करना XSS को पूर्ण रिमोट कोड निष्पादन (RCE) में बढ़ा सकता है। `vue-template-compiler` में CVEs जोखिम को साबित करते हैं।
```js
// DANGER never do this
const app = createSSRApp({ template: userProvidedHtml })
```
### Filters / render functions that eval
पुरानी फ़िल्टर जो रेंडर स्ट्रिंग बनाती हैं या उपयोगकर्ता डेटा पर `eval`/`new Function` को कॉल करती हैं, एक और XSS वेक्टर हैं—इन्हें संगणित गुणों से बदलें।
```js
Vue.filter('run', code => eval(code)) // DANGER
```
---
## Other Common Vulnerabilities in Vue Projects
### Prototype pollution in plugins
कुछ प्लगइन्स (जैसे, **vue-i18n**) में डीप-मर्ज हेल्पर्स ने हमलावरों को `Object.prototype` में लिखने की अनुमति दी है।
```js
import merge from 'deepmerge'
merge({}, JSON.parse('{ "__proto__": { "polluted": true } }'))
```
### Open redirects with vue-router
Unchecked उपयोगकर्ता URLs को `router.push` या `<router-link>` में पास करना `javascript:` URIs या फ़िशिंग डोमेन पर रीडायरेक्ट कर सकता है।
```js
this.$router.push(this.$route.query.next) // DANGER
```
### CSRF in Axios / fetch
SPAs को अभी भी सर्वर-साइड CSRF टोकन की आवश्यकता होती है; केवल SameSite कुकीज़ स्वचालित रूप से प्रस्तुत किए गए क्रॉस-ओरिजिन POST को रोक नहीं सकती हैं।
```js
axios.post('/api/transfer', data, {
headers: { 'X-CSRF-TOKEN': token }
})
```
### Click-jacking
Vue ऐप्स फ्रेम करने योग्य होते हैं जब तक आप दोनों `X-Frame-Options: DENY` और `Content-Security-Policy: frame-ancestors 'none'` नहीं भेजते।
```http
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none';
```
### Content-Security-Policy pitfalls
पूर्ण Vue निर्माण को `unsafe-eval` की आवश्यकता होती है; आप रनटाइम निर्माण या पूर्व-निर्मित टेम्पलेट्स पर स्विच करें ताकि आप उस खतरनाक स्रोत को हटा सकें।
```http
Content-Security-Policy: default-src 'self'; script-src 'self';
```
### Supply-chain attacks (node-ipc March 2022)
**node-ipc** का सबोटाज—जो Vue CLI द्वारा खींचा गया—ने दिखाया कि एक ट्रांजिटिव डिपेंडेंसी कैसे डेवलपमेंट मशीनों पर मनमाना कोड चला सकती है। संस्करणों को पिन करें और अक्सर ऑडिट करें।
```shell
npm ci --ignore-scripts # safer install
```
---
## Hardening Checklist
1. **हर स्ट्रिंग को `v-html` पर पहुँचने से पहले साफ करें** (DOMPurify)।
2. **अनुमत स्कीम, विशेषताएँ, घटक, और घटनाओं की सफेद सूची बनाएं**
3. **`eval` और गतिशील टेम्पलेट्स से पूरी तरह बचें**।
4. **साप्ताहिक रूप से निर्भरताओं को पैच करें** और सलाहों की निगरानी करें।
5. **मजबूत HTTP हेडर भेजें** (CSP, HSTS, XFO, CSRF)।
6. **ऑडिट, लॉकफाइल, और साइन किए गए कमिट्स के साथ अपनी सप्लाई चेन को लॉक करें**
## References
- [https://www.stackhawk.com/blog/vue-xss-guide-examples-and-prevention/](https://www.stackhawk.com/blog/vue-xss-guide-examples-and-prevention/)
- [https://medium.com/@isaacwangethi30/vue-js-security-6e246a7613da](https://medium.com/@isaacwangethi30/vue-js-security-6e246a7613da)
- [https://vuejs.org/guide/best-practices/security](https://vuejs.org/guide/best-practices/security)
{{#include ../../banners/hacktricks-training.md}}

View File

@ -1,108 +1,259 @@
/**
* HackTricks AI Chat Widget v1.14 animated typing indicator
* HackTricks AI Chat Widget v1.15 Markdown rendering + sanitised
* ------------------------------------------------------------------------
* Replaces the static placeholder with a threedot **bouncing** loader
* while waiting for the assistants response.
* Replaces the static placeholder with a three-dot **bouncing** loader
* Renders assistant replies as Markdown while purging any unsafe HTML
* (XSS-safe via DOMPurify)
* ------------------------------------------------------------------------
*/
(function () {
const LOG = "[HackTricks-AI]";
/* ---------------- Usertunable constants ---------------- */
const MAX_CONTEXT = 3000; // highlightedtext char limit
const MAX_QUESTION = 500; // question char limit
const TOOLTIP_TEXT =
"💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it";
const API_BASE = "https://www.hacktricks.ai/api/assistants/threads";
const BRAND_RED = "#b31328"; // HackTricks brand
/* ------------------------------ State ------------------------------ */
let threadId = null;
let isRunning = false;
const $ = (sel, ctx = document) => ctx.querySelector(sel);
if (document.getElementById("ht-ai-btn")) { console.warn(`${LOG} Widget already injected.`); return; }
(document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", init) : init());
/* ==================================================================== */
async function init() {
console.log(`${LOG} Injecting widget… v1.14`);
await ensureThreadId();
injectStyles();
const btn = createFloatingButton();
createTooltip(btn);
const panel = createSidebar();
const chatLog = $("#ht-ai-chat");
const sendBtn = $("#ht-ai-send");
const inputBox = $("#ht-ai-question");
const resetBtn = $("#ht-ai-reset");
const closeBtn = $("#ht-ai-close");
/* ------------------- Selection snapshot ------------------- */
let savedSelection = "";
btn.addEventListener("pointerdown", () => { savedSelection = window.getSelection().toString().trim(); });
/* ------------------- Helpers ------------------------------ */
function addMsg(text, cls) {
const b = document.createElement("div");
b.className = `ht-msg ${cls}`;
b.textContent = text;
chatLog.appendChild(b);
chatLog.scrollTop = chatLog.scrollHeight;
return b;
}
const LOADER_HTML = '<span class="ht-loading"><span></span><span></span><span></span></span>';
function setInputDisabled(d) { inputBox.disabled = d; sendBtn.disabled = d; }
function clearThreadCookie() { document.cookie = "threadId=; Path=/; Max-Age=0"; threadId = null; }
function resetConversation() { chatLog.innerHTML=""; clearThreadCookie(); panel.classList.remove("open"); }
/* ------------------- Panel open / close ------------------- */
btn.addEventListener("click", () => {
if (!savedSelection) { alert("Please highlight some text first to then ask Hacktricks AI about it."); return; }
if (savedSelection.length > MAX_CONTEXT) { alert(`Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.`); return; }
chatLog.innerHTML=""; addMsg(savedSelection, "ht-context"); panel.classList.add("open"); inputBox.focus();
});
closeBtn.addEventListener("click", resetConversation);
resetBtn.addEventListener("click", resetConversation);
/* --------------------------- Messaging --------------------------- */
async function sendMessage(question, context=null) {
if (!threadId) await ensureThreadId();
if (isRunning) { addMsg("Please wait until the current operation completes.", "ht-ai"); return; }
isRunning = true; setInputDisabled(true);
const loadingBubble = addMsg("", "ht-ai");
loadingBubble.innerHTML = LOADER_HTML;
const content = context ? `### Context:\n${context}\n\n### Question to answer:\n${question}` : question;
try {
const res = await fetch(`${API_BASE}/${threadId}/messages`, { method:"POST", credentials:"include", headers:{"Content-Type":"application/json"}, body:JSON.stringify({content}) });
if (!res.ok) {
let err=`Unknown error: ${res.status}`;
try { const e=await res.json(); if(e.error) err=`Error: ${e.error}`; else if(res.status===429) err="Rate limit exceeded. Please try again later."; } catch(_){}
loadingBubble.textContent = err; return; }
const data = await res.json();
loadingBubble.remove();
if (Array.isArray(data.response)) data.response.forEach(p=>{ addMsg( p.type==="text"&&p.text&&p.text.value ? p.text.value : JSON.stringify(p), "ht-ai"); });
else if (typeof data.response === "string") addMsg(data.response, "ht-ai");
else addMsg(JSON.stringify(data,null,2), "ht-ai");
} catch (e) { console.error("Error sending message:",e); loadingBubble.textContent="An unexpected error occurred."; }
finally { isRunning=false; setInputDisabled(false); chatLog.scrollTop=chatLog.scrollHeight; }
}
async function handleSend(){ const q=inputBox.value.trim(); if(!q)return; if(q.length>MAX_QUESTION){alert(`Your question is too long (${q.length} chars). Max allowed: ${MAX_QUESTION}.`); return;} inputBox.value=""; addMsg(q,"ht-user"); await sendMessage(q,savedSelection||null);}
sendBtn.addEventListener("click", handleSend);
inputBox.addEventListener("keydown", e=>{ if(e.key==="Enter"&&!e.shiftKey){ e.preventDefault(); handleSend(); } });
const LOG = "[HackTricks-AI]";
/* ---------------- User-tunable constants ---------------- */
const MAX_CONTEXT = 3000; // highlighted-text char limit
const MAX_QUESTION = 500; // question char limit
const TOOLTIP_TEXT =
"💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it";
const API_BASE = "https://www.hacktricks.ai/api/assistants/threads";
const BRAND_RED = "#b31328"; // HackTricks brand
/* ------------------------------ State ------------------------------ */
let threadId = null;
let isRunning = false;
const $ = (sel, ctx = document) => ctx.querySelector(sel);
if (document.getElementById("ht-ai-btn")) {
console.warn(`${LOG} Widget already injected.`);
return;
}
(document.readyState === "loading"
? document.addEventListener("DOMContentLoaded", init)
: init());
/* ==================================================================== */
/* 🔗 1. 3rd-party libs → Markdown & sanitiser */
/* ==================================================================== */
function loadScript(src) {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.onload = resolve;
s.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(s);
});
}
async function ensureDeps() {
const deps = [];
if (typeof marked === "undefined") {
deps.push(loadScript("https://cdn.jsdelivr.net/npm/marked/marked.min.js"));
}
/* ==================================================================== */
async function ensureThreadId(){ const m=document.cookie.match(/threadId=([^;]+)/); if(m&&m[1]){threadId=m[1];return;} try{ const r=await fetch(API_BASE,{method:"POST",credentials:"include"}); const d=await r.json(); if(!r.ok||!d.threadId) throw new Error(`${r.status} ${r.statusText}`); threadId=d.threadId; document.cookie=`threadId=${threadId}; Path=/; Secure; SameSite=Strict; Max-Age=7200`; }catch(e){ console.error("Error creating threadId:",e); alert("Failed to initialise the conversation. Please refresh and try again."); throw e; }}
/* ==================================================================== */
function injectStyles(){ const css=`
if (typeof DOMPurify === "undefined") {
deps.push(
loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.5/purify.min.js"
)
);
}
if (deps.length) await Promise.all(deps);
}
function mdToSafeHTML(md) {
// 1⃣ Markdown → raw HTML
const raw = marked.parse(md, { mangle: false, headerIds: false });
// 2⃣ Purify
return DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } });
}
/* ==================================================================== */
async function init() {
/* ----- make sure marked & DOMPurify are ready before anything else */
try {
await ensureDeps();
} catch (e) {
console.error(`${LOG} Could not load dependencies`, e);
return;
}
console.log(`${LOG} Injecting widget… v1.15`);
await ensureThreadId();
injectStyles();
const btn = createFloatingButton();
createTooltip(btn);
const panel = createSidebar();
const chatLog = $("#ht-ai-chat");
const sendBtn = $("#ht-ai-send");
const inputBox = $("#ht-ai-question");
const resetBtn = $("#ht-ai-reset");
const closeBtn = $("#ht-ai-close");
/* ------------------- Selection snapshot ------------------- */
let savedSelection = "";
btn.addEventListener("pointerdown", () => {
savedSelection = window.getSelection().toString().trim();
});
/* ------------------- Helpers ------------------------------ */
function addMsg(text, cls) {
const b = document.createElement("div");
b.className = `ht-msg ${cls}`;
// ✨ assistant replies rendered as Markdown + sanitised
if (cls === "ht-ai") {
b.innerHTML = mdToSafeHTML(text);
} else {
// user / context bubbles stay plain-text
b.textContent = text;
}
chatLog.appendChild(b);
chatLog.scrollTop = chatLog.scrollHeight;
return b;
}
const LOADER_HTML =
'<span class="ht-loading"><span></span><span></span><span></span></span>';
function setInputDisabled(d) {
inputBox.disabled = d;
sendBtn.disabled = d;
}
function clearThreadCookie() {
document.cookie = "threadId=; Path=/; Max-Age=0";
threadId = null;
}
function resetConversation() {
chatLog.innerHTML = "";
clearThreadCookie();
panel.classList.remove("open");
}
/* ------------------- Panel open / close ------------------- */
btn.addEventListener("click", () => {
if (!savedSelection) {
alert("Please highlight some text first to then ask HackTricks AI about it.");
return;
}
if (savedSelection.length > MAX_CONTEXT) {
alert(
`Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.`
);
return;
}
chatLog.innerHTML = "";
addMsg(savedSelection, "ht-context");
panel.classList.add("open");
inputBox.focus();
});
closeBtn.addEventListener("click", resetConversation);
resetBtn.addEventListener("click", resetConversation);
/* --------------------------- Messaging --------------------------- */
async function sendMessage(question, context = null) {
if (!threadId) await ensureThreadId();
if (isRunning) {
addMsg("Please wait until the current operation completes.", "ht-ai");
return;
}
isRunning = true;
setInputDisabled(true);
const loadingBubble = addMsg("", "ht-ai");
loadingBubble.innerHTML = LOADER_HTML;
const content = context
? `### Context:\n${context}\n\n### Question to answer:\n${question}`
: question;
try {
const res = await fetch(`${API_BASE}/${threadId}/messages`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content })
});
if (!res.ok) {
let err = `Unknown error: ${res.status}`;
try {
const e = await res.json();
if (e.error) err = `Error: ${e.error}`;
else if (res.status === 429)
err = "Rate limit exceeded. Please try again later.";
} catch (_) {}
loadingBubble.textContent = err;
return;
}
const data = await res.json();
loadingBubble.remove();
if (Array.isArray(data.response))
data.response.forEach((p) => {
addMsg(
p.type === "text" && p.text && p.text.value
? p.text.value
: JSON.stringify(p),
"ht-ai"
);
});
else if (typeof data.response === "string")
addMsg(data.response, "ht-ai");
else addMsg(JSON.stringify(data, null, 2), "ht-ai");
} catch (e) {
console.error("Error sending message:", e);
loadingBubble.textContent = "An unexpected error occurred.";
} finally {
isRunning = false;
setInputDisabled(false);
chatLog.scrollTop = chatLog.scrollHeight;
}
}
async function handleSend() {
const q = inputBox.value.trim();
if (!q) return;
if (q.length > MAX_QUESTION) {
alert(
`Your question is too long (${q.length} chars). Max allowed: ${MAX_QUESTION}.`
);
return;
}
inputBox.value = "";
addMsg(q, "ht-user");
await sendMessage(q, savedSelection || null);
}
sendBtn.addEventListener("click", handleSend);
inputBox.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
}
/* ==================================================================== */
async function ensureThreadId() {
const m = document.cookie.match(/threadId=([^;]+)/);
if (m && m[1]) {
threadId = m[1];
return;
}
try {
const r = await fetch(API_BASE, { method: "POST", credentials: "include" });
const d = await r.json();
if (!r.ok || !d.threadId) throw new Error(`${r.status} ${r.statusText}`);
threadId = d.threadId;
document.cookie =
`threadId=${threadId}; Path=/; Secure; SameSite=Strict; Max-Age=7200`;
} catch (e) {
console.error("Error creating threadId:", e);
alert("Failed to initialise the conversation. Please refresh and try again.");
throw e;
}
}
/* ==================================================================== */
function injectStyles() {
const css = `
#ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);width:60px;height:60px;border-radius:50%;background:#1e1e1e;color:#fff;font-size:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s}
#ht-ai-btn:hover{opacity:.85}
@media(max-width:768px){#ht-ai-btn{display:none}}
@ -132,10 +283,50 @@
@keyframes ht-bounce{0%,80%,100%{transform:scale(0);}40%{transform:scale(1);} }
::selection{background:#ffeb3b;color:#000}
::-moz-selection{background:#ffeb3b;color:#000}`;
const s=document.createElement("style"); s.id="ht-ai-style"; s.textContent=css; document.head.appendChild(s);}
function createFloatingButton(){ const d=document.createElement("div"); d.id="ht-ai-btn"; d.textContent="🤖"; document.body.appendChild(d); return d; }
function createTooltip(btn){ const t=document.createElement("div"); t.id="ht-ai-tooltip"; t.textContent=TOOLTIP_TEXT; document.body.appendChild(t); btn.addEventListener("mouseenter",()=>{const r=btn.getBoundingClientRect(); t.style.left=`${r.left+r.width/2}px`; t.style.top=`${r.top}px`; t.classList.add("show");}); btn.addEventListener("mouseleave",()=>t.classList.remove("show")); }
function createSidebar(){ const p=document.createElement("div"); p.id="ht-ai-panel"; p.innerHTML=`<div id="ht-ai-header"><strong>HackTricksAI Chat</strong><div class="ht-actions"><button id="ht-ai-reset" title="Reset">↺</button><span id="ht-ai-close" title="Close">✖</span></div></div><div id="ht-ai-chat"></div><div id="ht-ai-input"><textarea id="ht-ai-question" placeholder="Type your question…"></textarea><button id="ht-ai-send">Send</button></div>`; document.body.appendChild(p); return p; }
})();
const s = document.createElement("style");
s.id = "ht-ai-style";
s.textContent = css;
document.head.appendChild(s);
}
function createFloatingButton() {
const d = document.createElement("div");
d.id = "ht-ai-btn";
d.textContent = "🤖";
document.body.appendChild(d);
return d;
}
function createTooltip(btn) {
const t = document.createElement("div");
t.id = "ht-ai-tooltip";
t.textContent = TOOLTIP_TEXT;
document.body.appendChild(t);
btn.addEventListener("mouseenter", () => {
const r = btn.getBoundingClientRect();
t.style.left = `${r.left + r.width / 2}px`;
t.style.top = `${r.top}px`;
t.classList.add("show");
});
btn.addEventListener("mouseleave", () => t.classList.remove("show"));
}
function createSidebar() {
const p = document.createElement("div");
p.id = "ht-ai-panel";
p.innerHTML = `
<div id="ht-ai-header"><strong>HackTricks AI Chat</strong>
<div class="ht-actions">
<button id="ht-ai-reset" title="Reset"></button>
<span id="ht-ai-close" title="Close"></span>
</div>
</div>
<div id="ht-ai-chat"></div>
<div id="ht-ai-input">
<textarea id="ht-ai-question" placeholder="Type your question…"></textarea>
<button id="ht-ai-send">Send</button>
</div>`;
document.body.appendChild(p);
return p;
}
})();