Is Grok Automation - Auto Grok on Grok.com safe?
Grok Automation is medium risk. Starting a Grok job, the extension asks its background for a remote config: a GET to configs.kylenguyen.me/config/grok-automation, falling back to onegreen.workers.dev. It supplies control selectors; without one, automation stops.
Who publishes itTruong Nguyen - 4 other listings from the same operator, 1 of them carrying a finding
Truong Nguyen - 4 other listings from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
4 other listings published from this account, 186k+ users between them. 1 of them carries a finding.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Remote config controls Grok automation
Starting a Grok job, the extension asks its background for a remote config: a GET to configs.kylenguyen.me/config/grok-automation, falling back to onegreen.workers.dev.
It supplies control selectors; without one, automation stops.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You start a Grok automation job in the extension.
The job does not proceed with only the local extension code.
The extension requests a remote configuration and uses the returned selectors to drive the Grok page.
If the configuration is unavailable, the content script reports that it cannot connect to the server.
Observed during dynamic analysis: the endpoint returned JSON with shareUrlTemplate, including an imagine-public.x.ai path ending in {uuid}.mp4, and selectors for imagineLink, fileInput, and imageUploading.
- X-Client-Secret
- <redacted>
- Configuration hostconfigs.kylenguyen.me
This is where the extension asks for instructions before it runs your automation job.
- Video URL patternimagine-public.x.ai path ending in {uuid}.mp4
This pattern tells the extension how generated video links should be recognized or formatted.
- Imagine-page selectorselectors.imagineLink
This selector tells the extension which Grok page link to click when it redirects your job into the image or video flow.
- Image upload selectorselectors.fileInput
This selector points the extension at the file input used to upload images into your Grok prompt.
- Upload progress selectorselectors.imageUploading
This selector tells the extension how to wait while Grok is still processing an uploaded image.
The background module fetches remote configuration before automation runs
Readable config fetcher
assets/remoteConfig-Bs4jYha-.jsfunction t(t, n) { return t.version.split(",").map(t => t.trim()).filter(Boolean).includes(n.trim())}let n = null;async function e(t) { const n = await fetch(`${t}/config/grok-automation`, { method: "GET", headers: { "X-Client-Secret": "<redacted>" } }); if (!n.ok) throw new Error(`HTTP ${n.status}`); const e = await n.json(); if (!e?.selectors) throw new Error("Invalid config shape"); return e}async function r() { if (n) return n; try { return n = await e("https://configs.kylenguyen.me"), n } catch (t) {} try { return n = await e("https://extension-config.onegreen.workers.dev"), n } catch (t) { return null }}export { r as g, t as i};The content script gates automation on the remote config and consumes returned selectors
Automation queue waits for GET_REMOTE_CONFIG
assets/index.ts-BVcYzrtz.jsasync function z(e) { const t = await new Promise(e => { chrome.runtime.sendMessage({ type: "GET_REMOTE_CONFIG" }, t => e(t ?? null)) }); if (!t) return e.status = "error", G(e), B.shift(), e.sendResponse({ success: !1, error: "Cannot connect to server. Extension may be unauthorized.", results: [] }), U = null, void V(1); for (;;) { if (e.isCancelling) { e.status = "cancelled"; const t = e.results; return B.shift(), G(e), e.sendResponse({ success: !1, cancelled: !0, results: t }), U = null, void V(1) } if (e.pendingIndexes.length > 0) { const n = []; for (let r = 0; r < 1; r++) n.push($(e, t, W, G).catch(e => {})); await Promise.all(n) } for (; W.some(t => t.id === e.id);) await F(200); if (0 === e.pendingIndexes.length) { e.status = "completed"; const t = e.results.every(e => e.success), n = e.results; return B.shift(), G(e), e.sendResponse({ success: t, results: n }), U = null, void V(1) } }}Remote selectors drive page navigation and image upload
assets/index.ts-BVcYzrtz.jsI = async e => { try { b("Redirecting to imagine page..."); const t = window.location.href; return !(!t.includes("/imagine") || t.includes("/imagine/") || t.includes("saved")) || (await m(e.selectors.imagineLink, "Imagine link", 1e4), !0) } catch (t) { return E("Error in redirectToImaginePage:", t), !1 }}k = async (e, t, n, r) => { const o = n.selectors; return await m(n.selectors.plusImageButton, "Upload image button"), await (async (e, t, n) => { try { if (!e.images || 0 === e.images.length) return C("No images provided in config"), !1; if (t >= e.images.length || t < 0) return C(`Image index ${t} out of range. Total images: ${e.images.length}`), !1; const r = e.images[t]; if (!r || !r.base64) return C("No image base64 provided"), !1; let o = r.base64 || ""; o.includes(",") && (o = o.split(",")[1]); const i = atob(o), s = new Uint8Array(i.length); for (let e = 0; e < i.length; e++) s[e] = i.charCodeAt(e); const a = new Blob([s], { type: "image/jpeg" }), u = new File([a], r.name || `image-${Date.now()}.jpg`, { type: "image/jpeg" }); b(`Creating file: ${u.name}, size: ${u.size} bytes`); const c = n.selectors, l = await h(c.fileInput, 1e4, 100, !1); if (!l || 0 === l.length) return C("Could not find file input for image upload"), !1; const d = l.first().get(0), p = new DataTransfer; return p.items.add(u), d ? (d.files = p.files, d.dispatchEvent(new Event("change", { bubbles: !0 })), b("\u2705 Image file set and event triggered. Function will now complete."), !0) : (E("Could not get native input element to dispatch event."), !1) } catch (r) { return E("Error uploading image from base64:", r), !1 } })(e, t, n), (!r || !r()) && (b("Waiting for image upload to complete..."), await p(3e3), await (async (e, t = 3e4, n = 200) => { const r = Date.now(); for (; Date.now() - r < t;) { const t = i(e); if (!f(t)) return !0; await p(n) } return !1 })(o.imageUploading, 3e4), !(await h(o.imageUploadFailed, 1e3)) || (C("Image upload failed"), await m(o.removeFailedUploadedImage, "Remove failed uploaded image"), !1))}- configs.kylenguyen.me
Primary configuration host observed during dynamic analysis and present in the shipped config fetcher.
- extension-config.onegreen.workers.dev
Fallback configuration host present in the shipped config fetcher if the primary host fails.
- imagine-public.x.ai
Host pattern recorded in the captured response summary for generated video URLs.