Загрузка данных
--[[
CLIENT EXPOSURE DEBUGGER v4.0
Defensive client-surface auditor for your own Roblox place.
Design goals:
* Read-only: never fires remotes, prompts, click detectors, or mutates game state.
* No executor-specific APIs (no getgc/getsenv/debug registry/hooks/decompiler/etc.).
* Scans the place-relevant client-visible DataModel.
* Categorizes findings instead of dumping one huge text blob.
* Virtualized result list: only a small fixed pool of GUI rows exists at once.
* Live-watch for newly replicated/streamed-in descendants.
* Redacts values that look like secrets instead of printing them in full.
* Close button uses Activated + MouseButton1Click fallback and full cleanup.
IMPORTANT THREAT-MODEL NOTE:
Presence of a finding is not proof of an exploit. Severity is REVIEW PRIORITY.
Anything replicated to a compromised client must be treated as readable/controllable
according to Roblox's client/server security model.
]]
local VERSION = "4.0"
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local CollectionService = game:GetService("CollectionService")
local localPlayer = Players.LocalPlayer
if not localPlayer then
warn("[Client Exposure Debugger] LocalPlayer is unavailable. Run this on the client.")
return
end
local playerGui = localPlayer:WaitForChild("PlayerGui")
local CONFIG = {
GUI_NAME = "client_exposure_debugger_v4",
MAX_INSTANCES = 150000,
SCAN_YIELD_EVERY = 300,
ROW_HEIGHT = 30,
ROW_POOL_SIZE = 48,
PRINT_LIMIT = 5000,
LIVE_REFRESH_INTERVAL = 0.35,
}
local COLORS = {
bg = Color3.fromRGB(15, 16, 20),
panel = Color3.fromRGB(22, 23, 29),
panel2 = Color3.fromRGB(29, 30, 37),
panel3 = Color3.fromRGB(36, 37, 45),
stroke = Color3.fromRGB(62, 64, 76),
text = Color3.fromRGB(235, 236, 242),
dim = Color3.fromRGB(157, 161, 176),
accent = Color3.fromRGB(104, 136, 255),
info = Color3.fromRGB(123, 180, 255),
low = Color3.fromRGB(155, 190, 130),
medium = Color3.fromRGB(239, 188, 95),
high = Color3.fromRGB(243, 126, 93),
critical = Color3.fromRGB(255, 84, 108),
selected = Color3.fromRGB(50, 56, 74),
}
local SEVERITY_RANK = {
INFO = 0,
LOW = 1,
MEDIUM = 2,
HIGH = 3,
CRITICAL = 4,
}
local SEVERITY_COLOR = {
INFO = COLORS.info,
LOW = COLORS.low,
MEDIUM = COLORS.medium,
HIGH = COLORS.high,
CRITICAL = COLORS.critical,
}
local CATEGORY_ORDER = {
"ALL",
"RISKS",
"REMOTES",
"CODE",
"STATE",
"INTERACTIONS",
"PHYSICS",
"UI",
"ASSETS",
"SYSTEM",
}
local CATEGORY_LABEL = {
ALL = "All findings",
RISKS = "Review queue",
REMOTES = "Remotes / IPC",
CODE = "Client code",
STATE = "State / variables",
INTERACTIONS = "Interactions",
PHYSICS = "Physics",
UI = "UI exposure",
ASSETS = "Assets",
SYSTEM = "System notes",
}
local SECRET_WORDS = {
"secret", "token", "password", "passwd", "apikey", "api_key",
"privatekey", "private_key", "webhook", "authorization", "authkey",
"bearer", "credential",
}
local SENSITIVE_WORDS = {
"admin", "owner", "moderator", "ban", "kick", "grant", "give",
"reward", "purchase", "buy", "sell", "money", "cash", "coin",
"currency", "inventory", "item", "weapon", "damage", "cooldown",
"teleport", "speed", "level", "xp", "experience", "save", "load",
"data", "profile", "anticheat", "anti_cheat", "security", "permission",
}
local ASSET_PROPERTIES = {
Sound = {"SoundId"},
Animation = {"AnimationId"},
MeshPart = {"MeshId", "TextureID"},
SpecialMesh = {"MeshId", "TextureId"},
ImageLabel = {"Image"},
ImageButton = {"Image"},
Decal = {"Texture"},
Texture = {"Texture"},
ParticleEmitter = {"Texture"},
Beam = {"Texture"},
Trail = {"Texture"},
VideoFrame = {"Video"},
Shirt = {"ShirtTemplate"},
Pants = {"PantsTemplate"},
SurfaceAppearance = {"ColorMap", "NormalMap", "MetalnessMap", "RoughnessMap"},
}
local PHYSICS_CLASS_NAMES = {
LinearVelocity = true,
AngularVelocity = true,
VectorForce = true,
LineForce = true,
Torque = true,
AlignPosition = true,
AlignOrientation = true,
BodyVelocity = true,
BodyAngularVelocity = true,
BodyPosition = true,
BodyGyro = true,
RocketPropulsion = true,
}
-- Cleanup previous copy.
local oldGui = playerGui:FindFirstChild(CONFIG.GUI_NAME)
if oldGui then
oldGui:Destroy()
end
local connections = {}
local closed = false
local scanGeneration = 0
local scanning = false
local liveDirty = false
local lastLiveRefresh = 0
local function trackConnection(connection)
table.insert(connections, connection)
return connection
end
local function safeDisconnect(connection)
pcall(function()
connection:Disconnect()
end)
end
local function new(className, props, parent)
local object = Instance.new(className)
if props then
for key, value in pairs(props) do
object[key] = value
end
end
if parent then
object.Parent = parent
end
return object
end
local function addCorner(parent, radius)
return new("UICorner", {CornerRadius = UDim.new(0, radius or 8)}, parent)
end
local function addStroke(parent, color, thickness, transparency)
return new("UIStroke", {
Color = color or COLORS.stroke,
Thickness = thickness or 1,
Transparency = transparency or 0,
}, parent)
end
local function safeGet(object, property)
local ok, value = pcall(function()
return object[property]
end)
if ok then
return value
end
return nil
end
local function safeFullName(object)
local ok, value = pcall(function()
return object:GetFullName()
end)
if ok then
return value
end
return object.Name
end
local function lower(value)
return string.lower(tostring(value or ""))
end
local function containsAny(text, words)
text = lower(text)
for _, word in ipairs(words) do
if string.find(text, word, 1, true) then
return true, word
end
end
return false, nil
end
local function isSecretContext(text)
local found = containsAny(text, SECRET_WORDS)
return found
end
local function isSensitiveContext(text)
local found = containsAny(text, SENSITIVE_WORDS)
return found
end
local function looksLikeSecretValue(value)
if typeof(value) ~= "string" then
return false
end
local text = lower(value)
if string.find(text, "discord.com/api/webhooks/", 1, true)
or string.find(text, "discordapp.com/api/webhooks/", 1, true)
or string.find(text, "authorization: bearer", 1, true)
or string.find(text, "private key", 1, true)
then
return true
end
return false
end
local function valueToString(value, context)
local valueType = typeof(value)
if isSecretContext(context) or looksLikeSecretValue(value) then
local length = valueType == "string" and #value or 0
if length > 0 then
return string.format("<REDACTED client-visible value; length=%d>", length)
end
return "<REDACTED client-visible value>"
end
if valueType == "Instance" then
return "<Instance> " .. safeFullName(value)
end
if valueType == "string" then
local text = value
if #text > 500 then
text = string.sub(text, 1, 500) .. "…"
end
return text
end
local ok, result = pcall(function()
return tostring(value)
end)
if ok then
return result
end
return "<unprintable " .. valueType .. ">"
end
-- ============================================================
-- GUI
-- ============================================================
local gui = new("ScreenGui", {
Name = CONFIG.GUI_NAME,
ResetOnSpawn = false,
IgnoreGuiInset = true,
DisplayOrder = 100000,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}, playerGui)
local main = new("Frame", {
Name = "window",
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(0.94, 0, 0.88, 0),
BackgroundColor3 = COLORS.bg,
BorderSizePixel = 0,
ClipsDescendants = true,
ZIndex = 10,
}, gui)
addCorner(main, 12)
addStroke(main, COLORS.stroke, 1, 0)
local header = new("Frame", {
Name = "header",
Size = UDim2.new(1, 0, 0, 48),
BackgroundColor3 = COLORS.panel,
BorderSizePixel = 0,
Active = true,
ZIndex = 100,
}, main)
local title = new("TextLabel", {
Position = UDim2.fromOffset(16, 0),
Size = UDim2.new(1, -330, 1, 0),
BackgroundTransparency = 1,
Text = "CLIENT EXPOSURE DEBUGGER v" .. VERSION,
TextColor3 = COLORS.text,
Font = Enum.Font.Code,
TextSize = 18,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 110,
}, header)
local headerStatus = new("TextLabel", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -58, 0, 0),
Size = UDim2.fromOffset(250, 48),
BackgroundTransparency = 1,
Text = "initializing…",
TextColor3 = COLORS.dim,
Font = Enum.Font.Code,
TextSize = 13,
TextXAlignment = Enum.TextXAlignment.Right,
ZIndex = 110,
}, header)
local closeButton = new("TextButton", {
Name = "close",
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, -8, 0, 7),
Size = UDim2.fromOffset(36, 34),
BackgroundColor3 = COLORS.panel3,
BorderSizePixel = 0,
Text = "×",
TextColor3 = COLORS.text,
Font = Enum.Font.GothamBold,
TextSize = 24,
AutoButtonColor = true,
Active = true,
ZIndex = 200,
}, header)
addCorner(closeButton, 8)
local sidebar = new("Frame", {
Position = UDim2.fromOffset(0, 48),
Size = UDim2.new(0, 202, 1, -48),
BackgroundColor3 = COLORS.panel,
BorderSizePixel = 0,
ZIndex = 20,
}, main)
local summaryLabel = new("TextLabel", {
Position = UDim2.fromOffset(12, 12),
Size = UDim2.new(1, -24, 0, 128),
BackgroundColor3 = COLORS.panel2,
BorderSizePixel = 0,
Text = "Waiting for scan…",
TextColor3 = COLORS.dim,
Font = Enum.Font.Code,
TextSize = 12,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
ZIndex = 25,
}, sidebar)
addCorner(summaryLabel, 8)
local tabsContainer = new("Frame", {
Position = UDim2.fromOffset(8, 150),
Size = UDim2.new(1, -16, 1, -282),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 25,
}, sidebar)
local tabLayout = new("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
Padding = UDim.new(0, 5),
SortOrder = Enum.SortOrder.LayoutOrder,
}, tabsContainer)
local rescanButton = new("TextButton", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 10, 1, -92),
Size = UDim2.new(1, -20, 0, 36),
BackgroundColor3 = COLORS.accent,
BorderSizePixel = 0,
Text = "RESCAN",
TextColor3 = Color3.new(1, 1, 1),
Font = Enum.Font.GothamBold,
TextSize = 13,
ZIndex = 30,
}, sidebar)
addCorner(rescanButton, 8)
local printButton = new("TextButton", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 10, 1, -50),
Size = UDim2.new(1, -20, 0, 34),
BackgroundColor3 = COLORS.panel3,
BorderSizePixel = 0,
Text = "PRINT FILTERED",
TextColor3 = COLORS.text,
Font = Enum.Font.GothamBold,
TextSize = 12,
ZIndex = 30,
}, sidebar)
addCorner(printButton, 8)
local hintLabel = new("TextLabel", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 12, 1, -8),
Size = UDim2.new(1, -24, 0, 34),
BackgroundTransparency = 1,
Text = "RightShift = hide/show\nEnd = close",
TextColor3 = COLORS.dim,
Font = Enum.Font.Code,
TextSize = 10,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
ZIndex = 25,
}, sidebar)
local detailsWidth = 350
local center = new("Frame", {
Position = UDim2.fromOffset(202, 48),
Size = UDim2.new(1, -(202 + detailsWidth), 1, -48),
BackgroundColor3 = COLORS.bg,
BorderSizePixel = 0,
ZIndex = 20,
}, main)
local toolsBar = new("Frame", {
Position = UDim2.fromOffset(10, 10),
Size = UDim2.new(1, -20, 0, 38),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = 30,
}, center)
local searchBox = new("TextBox", {
Position = UDim2.fromOffset(0, 0),
Size = UDim2.new(1, -178, 1, 0),
BackgroundColor3 = COLORS.panel2,
BorderSizePixel = 0,
PlaceholderText = "Search path / name / value / note…",
PlaceholderColor3 = COLORS.dim,
Text = "",
TextColor3 = COLORS.text,
Font = Enum.Font.Code,
TextSize = 13,
ClearTextOnFocus = false,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 35,
}, toolsBar)
addCorner(searchBox, 8)
new("UIPadding", {
PaddingLeft = UDim.new(0, 10),
PaddingRight = UDim.new(0, 10),
}, searchBox)
local severityButton = new("TextButton", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 0),
Size = UDim2.fromOffset(168, 38),
BackgroundColor3 = COLORS.panel2,
BorderSizePixel = 0,
Text = "Severity: ALL",
TextColor3 = COLORS.text,
Font = Enum.Font.Code,
TextSize = 12,
ZIndex = 35,
}, toolsBar)
addCorner(severityButton, 8)
local listFrame = new("ScrollingFrame", {
Position = UDim2.fromOffset(10, 56),
Size = UDim2.new(1, -20, 1, -90),
BackgroundColor3 = COLORS.panel,
BorderSizePixel = 0,
ScrollBarThickness = 6,
ScrollBarImageColor3 = COLORS.stroke,
CanvasSize = UDim2.fromOffset(0, 0),
AutomaticCanvasSize = Enum.AutomaticSize.None,
ScrollingDirection = Enum.ScrollingDirection.Y,
ElasticBehavior = Enum.ElasticBehavior.WhenScrollable,
ZIndex = 25,
}, center)
addCorner(listFrame, 8)
local emptyLabel = new("TextLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(1, -30, 0, 70),
BackgroundTransparency = 1,
Text = "No findings in this view.",
TextColor3 = COLORS.dim,
Font = Enum.Font.Code,
TextSize = 14,
TextWrapped = true,
ZIndex = 26,
}, listFrame)
local listFooter = new("TextLabel", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 10, 1, -6),
Size = UDim2.new(1, -20, 0, 22),
BackgroundTransparency = 1,
Text = "0 results",
TextColor3 = COLORS.dim,
Font = Enum.Font.Code,
TextSize = 11,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 30,
}, center)
local details = new("Frame", {
AnchorPoint = Vector2.new(1, 0),
Position = UDim2.new(1, 0, 0, 48),
Size = UDim2.new(0, detailsWidth, 1, -48),
BackgroundColor3 = COLORS.panel,
BorderSizePixel = 0,
ZIndex = 20,
}, main)
local detailsTitle = new("TextLabel", {
Position = UDim2.fromOffset(14, 12),
Size = UDim2.new(1, -28, 0, 28),
BackgroundTransparency = 1,
Text = "DETAILS",
TextColor3 = COLORS.text,
Font = Enum.Font.GothamBold,
TextSize = 14,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 25,
}, details)
local detailsBody = new("ScrollingFrame", {
Position = UDim2.fromOffset(12, 48),
Size = UDim2.new(1, -24, 1, -100),
BackgroundColor3 = COLORS.panel2,
BorderSizePixel = 0,
ScrollBarThickness = 5,
ScrollBarImageColor3 = COLORS.stroke,
AutomaticCanvasSize = Enum.AutomaticSize.Y,
CanvasSize = UDim2.fromOffset(0, 0),
ScrollingDirection = Enum.ScrollingDirection.Y,
ZIndex = 25,
}, details)
addCorner(detailsBody, 8)
local detailsText = new("TextLabel", {
Position = UDim2.fromOffset(10, 10),
Size = UDim2.new(1, -20, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1,
Text = "Select a finding to inspect it.\n\nSeverity is review priority, not proof of an exploit.",
TextColor3 = COLORS.text,
Font = Enum.Font.Code,
TextSize = 12,
TextWrapped = true,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
ZIndex = 26,
}, detailsBody)
local selectedPrintButton = new("TextButton", {
AnchorPoint = Vector2.new(0, 1),
Position = UDim2.new(0, 12, 1, -10),
Size = UDim2.new(1, -24, 0, 34),
BackgroundColor3 = COLORS.panel3,
BorderSizePixel = 0,
Text = "PRINT SELECTED",
TextColor3 = COLORS.text,
Font = Enum.Font.GothamBold,
TextSize = 12,
ZIndex = 25,
}, details)
addCorner(selectedPrintButton, 8)
-- ============================================================
-- Window close / hide / drag
-- ============================================================
local function shutdown()
if closed then
return
end
closed = true
scanGeneration = scanGeneration + 1
for _, connection in ipairs(connections) do
safeDisconnect(connection)
end
connections = {}
if gui and gui.Parent then
gui:Destroy()
end
end
-- Activated is the primary path. MouseButton1Click is a fallback for clients/executors
-- that have odd input routing. The guard in shutdown() makes double-fire harmless.
trackConnection(closeButton.Activated:Connect(shutdown))
trackConnection(closeButton.MouseButton1Click:Connect(shutdown))
local dragging = false
local dragStart = nil
local startPosition = nil
trackConnection(header.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch
then
dragging = true
dragStart = input.Position
startPosition = main.Position
end
end))
trackConnection(UserInputService.InputChanged:Connect(function(input)
if not dragging or not dragStart or not startPosition then
return
end
if input.UserInputType == Enum.UserInputType.MouseMovement
or input.UserInputType == Enum.UserInputType.Touch
then
local delta = input.Position - dragStart
main.Position = UDim2.new(
startPosition.X.Scale,
startPosition.X.Offset + delta.X,
startPosition.Y.Scale,
startPosition.Y.Offset + delta.Y
)
end
end))
trackConnection(UserInputService.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1
or input.UserInputType == Enum.UserInputType.Touch
then
dragging = false
dragStart = nil
startPosition = nil
end
end))
trackConnection(UserInputService.InputBegan:Connect(function(input, processed)
if processed then
return
end
if input.KeyCode == Enum.KeyCode.RightShift then
main.Visible = not main.Visible
elseif input.KeyCode == Enum.KeyCode.End then
shutdown()
end
end))
-- ============================================================
-- Data model / findings
-- ============================================================
local findings = {}
local filteredFindings = {}
local findingKeys = {}
local seenAssemblies = {}
local seenAssets = {}
local currentTab = "ALL"
local selectedFinding = nil
local severityModeIndex = 1
local SEVERITY_MODES = {
{label = "ALL", minRank = 0},
{label = "MEDIUM+", minRank = 2},
{label = "HIGH+", minRank = 3},
{label = "CRITICAL", minRank = 4},
}
local stats = {}
local function resetStats()
stats = {
instances = 0,
attributes = 0,
tags = 0,
values = 0,
remotes = 0,
code = 0,
interactions = 0,
assemblies = 0,
ui = 0,
assets = 0,
risks = 0,
high = 0,
critical = 0,
truncated = false,
}
end
resetStats()
local function makeFindingKey(category, path, title, key)
return table.concat({category or "", path or "", title or "", key or ""}, "\31")
end
local function addFinding(category, severity, titleText, object, key, value, note, risk)
if closed then
return
end
local path = object and safeFullName(object) or "<system>"
local uniqueKey = makeFindingKey(category, path, titleText, key)
if findingKeys[uniqueKey] then
return
end
findingKeys[uniqueKey] = true
local context = table.concat({path, titleText or "", key or ""}, " ")
local displayValue = value == nil and "" or valueToString(value, context)
local finding = {
category = category,
severity = severity,
rank = SEVERITY_RANK[severity] or 0,
title = titleText or "Finding",
path = path,
key = key or "",
value = displayValue,
note = note or "",
risk = risk == true,
className = object and object.ClassName or "System",
object = object,
}
table.insert(findings, finding)
if finding.risk then
stats.risks = stats.risks + 1
end
if finding.severity == "HIGH" then
stats.high = stats.high + 1
elseif finding.severity == "CRITICAL" then
stats.critical = stats.critical + 1
end
liveDirty = true
end
local function severityForContext(context, baseSeverity)
if isSecretContext(context) then
return "CRITICAL", true
end
if isSensitiveContext(context) then
local base = SEVERITY_RANK[baseSeverity] or 0
if base < SEVERITY_RANK.HIGH then
return "HIGH", true
end
end
return baseSeverity, (SEVERITY_RANK[baseSeverity] or 0) >= SEVERITY_RANK.LOW
end
local function safeGetTags(object)
local ok, tags = pcall(function()
return CollectionService:GetTags(object)
end)
if ok then
return tags
end
return nil
end
local function getPlayerFromDescendant(object)
local model = object:FindFirstAncestorOfClass("Model")
if model then
local ok, player = pcall(function()
return Players:GetPlayerFromCharacter(model)
end)
if ok then
return player
end
end
return nil
end
local function inspectAssetProperties(object)
for className, properties in pairs(ASSET_PROPERTIES) do
if object:IsA(className) then
for _, property in ipairs(properties) do
local value = safeGet(object, property)
if value ~= nil then
local text = tostring(value)
if text ~= "" then
local dedupeKey = className .. "|" .. property .. "|" .. text
if not seenAssets[dedupeKey] then
seenAssets[dedupeKey] = true
stats.assets = stats.assets + 1
addFinding(
"ASSETS",
"INFO",
className .. "." .. property,
object,
property,
value,
"Client-visible asset/content reference.",
false
)
end
end
end
end
end
end
end
local function inspectAttributes(object)
local ok, attributes = pcall(function()
return object:GetAttributes()
end)
if not ok then
return
end
for name, value in pairs(attributes) do
stats.attributes = stats.attributes + 1
local context = safeFullName(object) .. " " .. name
local severity, risk = severityForContext(context, "INFO")
local note = "Attribute is replicated with this instance and readable by the client."
if severity == "CRITICAL" then
note = "Potential secret-like client-visible attribute. Move secrets/server authority out of replicated state. Value is redacted here."
elseif severity == "HIGH" then
note = "Sensitive gameplay/state attribute. Treat it as untrusted client-visible information; server must remain authoritative."
end
addFinding("STATE", severity, "Attribute", object, name, value, note, risk)
end
end
local function inspectTags(object)
local tags = safeGetTags(object)
if not tags then
return
end
for _, tag in ipairs(tags) do
stats.tags = stats.tags + 1
local severity, risk = severityForContext(safeFullName(object) .. " " .. tag, "INFO")
addFinding(
"STATE",
severity,
"CollectionService tag",
object,
"tag",
tag,
"Tag names are visible to the client and can reveal gameplay classification/architecture.",
risk
)
end
end
local function inspectValueObject(object)
if not object:IsA("ValueBase") then
return
end
stats.values = stats.values + 1
local value = safeGet(object, "Value")
local context = safeFullName(object) .. " " .. object.Name
local severity, risk = severityForContext(context, "INFO")
local note = "ValueBase content is client-visible when the instance is replicated."
if severity == "CRITICAL" then
note = "Potential secret-like ValueBase. Never keep secrets in replicated instances. Value is redacted here."
elseif severity == "HIGH" then
note = "Sensitive gameplay/economy/config value is visible. Server must not trust client copies of this state."
end
addFinding("STATE", severity, object.ClassName, object, "Value", value, note, risk)
end
local function inspectRemote(object)
if object:IsA("RemoteEvent")
or object:IsA("RemoteFunction")
or object:IsA("UnreliableRemoteEvent")
then
stats.remotes = stats.remotes + 1
local context = safeFullName(object)
local severity, risk = severityForContext(context, "LOW")
local note = "Server boundary surface. Validate permission/context, types, ranges and rate limits server-side. Presence alone is not a vulnerability."
if object:IsA("RemoteFunction") then
local callback = safeGet(object, "OnClientInvoke")
if typeof(callback) == "function" then
note = note .. " A client OnClientInvoke callback is currently bound."
end
end
addFinding("REMOTES", severity, object.ClassName, object, "name", object.Name, note, risk)
return
end
if object:IsA("BindableEvent") or object:IsA("BindableFunction") then
addFinding(
"REMOTES",
"INFO",
object.ClassName .. " (local IPC)",
object,
"name",
object.Name,
"Bindable objects are client-local IPC when present on the client; they do not cross the client/server boundary by themselves.",
false
)
end
end
local function inspectCode(object)
local isClientCode = false
local codeType = nil
if object:IsA("LocalScript") then
isClientCode = true
codeType = "LocalScript"
elseif object:IsA("ModuleScript") then
isClientCode = true
codeType = "ModuleScript"
elseif object:IsA("Script") then
local runContext = safeGet(object, "RunContext")
if runContext == Enum.RunContext.Client then
isClientCode = true
codeType = "Script (RunContext=Client)"
end
end
if not isClientCode then
return
end
stats.code = stats.code + 1
local context = safeFullName(object)
local severity, risk = severityForContext(context, "LOW")
local note = "Replicated client code should be assumed readable/decompilable by a compromised client. This auditor intentionally does not read Source or decompile anything."
if severity == "CRITICAL" or severity == "HIGH" then
note = note .. " Sensitive/security-themed naming raises review priority: secrets and authoritative checks must remain server-only."
end
addFinding("CODE", severity, codeType, object, "Enabled", safeGet(object, "Enabled"), note, risk)
end
local function inspectInteraction(object)
if object:IsA("ProximityPrompt") then
stats.interactions = stats.interactions + 1
local summary = string.format(
"Action=%s | Object=%s | Hold=%.2f | MaxDistance=%.2f | Enabled=%s",
tostring(safeGet(object, "ActionText") or ""),
tostring(safeGet(object, "ObjectText") or ""),
tonumber(safeGet(object, "HoldDuration") or 0) or 0,
tonumber(safeGet(object, "MaxActivationDistance") or 0) or 0,
tostring(safeGet(object, "Enabled"))
)
addFinding(
"INTERACTIONS",
"MEDIUM",
"ProximityPrompt",
object,
"settings",
summary,
"Client-triggered interaction surface. Server must validate distance, state, permissions and rate independently of prompt settings.",
true
)
return
end
if object:IsA("ClickDetector") then
stats.interactions = stats.interactions + 1
addFinding(
"INTERACTIONS",
"MEDIUM",
"ClickDetector",
object,
"MaxActivationDistance",
safeGet(object, "MaxActivationDistance"),
"Client-triggered interaction surface. Do not rely on client-side distance or click legitimacy alone.",
true
)
return
end
if object:IsA("DragDetector") then
stats.interactions = stats.interactions + 1
addFinding(
"INTERACTIONS",
"MEDIUM",
"DragDetector",
object,
"Enabled",
safeGet(object, "Enabled"),
"Client interaction surface. Server-side state/permission validation is still required for consequential actions.",
true
)
end
end
local function inspectPhysics(object)
if object:IsA("BasePart") then
local anchored = safeGet(object, "Anchored")
if anchored == false then
local root = safeGet(object, "AssemblyRootPart") or object
if root and not seenAssemblies[root] then
seenAssemblies[root] = true
stats.assemblies = stats.assemblies + 1
local ownerPlayer = getPlayerFromDescendant(root)
local severity = "LOW"
local note = "Unanchored assembly is a network-ownership candidate. Roblox may assign client simulation authority; validate consequential physics/touch outcomes on the server."
if ownerPlayer == localPlayer then
severity = "MEDIUM"
note = "Local player's character assembly is client-simulated. Movement/physics anti-cheat must use server validation rather than trusting client state."
elseif ownerPlayer then
note = "Another player's character assembly is replicated client-side. Treat observed client state as non-authoritative."
end
local value = string.format(
"Root=%s | Mass=%s | CanCollide=%s | CanTouch=%s | CanQuery=%s",
safeFullName(root),
tostring(safeGet(root, "AssemblyMass")),
tostring(safeGet(root, "CanCollide")),
tostring(safeGet(root, "CanTouch")),
tostring(safeGet(root, "CanQuery"))
)
addFinding("PHYSICS", severity, "Unanchored assembly", root, "physics", value, note, true)
end
end
return
end
if PHYSICS_CLASS_NAMES[object.ClassName] then
addFinding(
"PHYSICS",
"LOW",
"Physics controller: " .. object.ClassName,
object,
"Enabled",
safeGet(object, "Enabled"),
"Client-visible physics controller/constraint. Review ownership and ensure server validates consequential motion/state.",
true
)
return
end
if object.ClassName == "TouchTransmitter" then
addFinding(
"PHYSICS",
"MEDIUM",
"TouchTransmitter",
object,
"type",
object.ClassName,
"Touch-based gameplay should be server-validated because client-owned physics can influence touch reports.",
true
)
end
end
local function inspectUI(object)
if object:IsA("ScreenGui") or object:IsA("SurfaceGui") or object:IsA("BillboardGui") then
stats.ui = stats.ui + 1
local value = string.format(
"Enabled=%s | DisplayOrder=%s | ResetOnSpawn=%s",
tostring(safeGet(object, "Enabled")),
tostring(safeGet(object, "DisplayOrder")),
tostring(safeGet(object, "ResetOnSpawn"))
)
addFinding(
"UI",
"INFO",
object.ClassName,
object,
"settings",
value,
"Client-visible GUI root. UI visibility/state is not an authority boundary.",
false
)
return
end
if object:IsA("TextButton") or object:IsA("TextLabel") or object:IsA("TextBox") then
local text = safeGet(object, "Text")
if text ~= nil and tostring(text) ~= "" then
stats.ui = stats.ui + 1
local context = safeFullName(object) .. " text"
local severity, risk = severityForContext(context, "INFO")
addFinding(
"UI",
severity,
object.ClassName .. " text",
object,
"Text",
text,
"Rendered/client-held text can reveal runtime state, labels, internal naming or values. Do not treat hidden UI as confidential.",
risk
)
end
return
end
if object:IsA("ImageButton") then
stats.ui = stats.ui + 1
addFinding(
"UI",
"INFO",
"ImageButton",
object,
"Visible",
safeGet(object, "Visible"),
"Client-visible clickable UI surface.",
false
)
end
end
local function inspectRuntimeState(object)
if object:IsA("Humanoid") then
local value = string.format(
"Health=%s/%s | WalkSpeed=%s | JumpPower=%s | HipHeight=%s",
tostring(safeGet(object, "Health")),
tostring(safeGet(object, "MaxHealth")),
tostring(safeGet(object, "WalkSpeed")),
tostring(safeGet(object, "JumpPower")),
tostring(safeGet(object, "HipHeight"))
)
addFinding(
"STATE",
"LOW",
"Humanoid runtime state",
object,
"state",
value,
"Humanoid properties are client-visible and some can be locally modified; server anti-cheat should validate outcomes rather than trust the local copy.",
true
)
elseif object:IsA("Tool") then
addFinding(
"STATE",
"INFO",
"Tool",
object,
"Enabled",
safeGet(object, "Enabled"),
"Client-visible inventory/tool structure can reveal available actions and metadata.",
false
)
elseif object:IsA("Configuration") then
addFinding(
"STATE",
"INFO",
"Configuration container",
object,
"name",
object.Name,
"Replicated Configuration structure is client-visible; inspect its attributes/children for sensitive state.",
false
)
end
end
local function isDebuggerObject(object)
if object == gui then
return true
end
local ok, result = pcall(function()
return object:IsDescendantOf(gui)
end)
return ok and result or false
end
local function inspectInstance(object)
if closed or not object or isDebuggerObject(object) then
return
end
stats.instances = stats.instances + 1
inspectAttributes(object)
inspectTags(object)
inspectValueObject(object)
inspectRemote(object)
inspectCode(object)
inspectInteraction(object)
inspectPhysics(object)
inspectUI(object)
inspectAssetProperties(object)
inspectRuntimeState(object)
end
-- ============================================================
-- Scope
-- ============================================================
local function tryGetService(name)
local ok, service = pcall(function()
return game:GetService(name)
end)
if ok then
return service
end
return nil
end
local scanRoots = {}
local scanRootSet = {}
local function addScanRoot(name)
local service = tryGetService(name)
if service and not scanRootSet[service] then
scanRootSet[service] = true
table.insert(scanRoots, service)
end
end
-- Place-relevant roots. Deliberately excludes Roblox engine-internal CoreGui/CorePackages noise.
addScanRoot("ReplicatedFirst")
addScanRoot("ReplicatedStorage")
addScanRoot("Workspace")
addScanRoot("Players")
addScanRoot("Lighting")
addScanRoot("SoundService")
addScanRoot("StarterGui")
addScanRoot("StarterPlayer")
addScanRoot("StarterPack")
addScanRoot("Teams")
addScanRoot("TextChatService")
addScanRoot("LocalizationService")
addScanRoot("MaterialService")
local function isInScope(object)
if isDebuggerObject(object) then
return false
end
for _, root in ipairs(scanRoots) do
if object == root then
return true
end
local ok, result = pcall(function()
return object:IsDescendantOf(root)
end)
if ok and result then
return true
end
end
return false
end
-- ============================================================
-- Filtering / virtualized list
-- ============================================================
local tabButtons = {}
local rowPool = {}
local rowIndexMap = {}
local function findingMatchesSearch(finding, query)
if query == "" then
return true
end
local haystack = lower(table.concat({
finding.category or "",
finding.severity or "",
finding.title or "",
finding.path or "",
finding.key or "",
finding.value or "",
finding.note or "",
finding.className or "",
}, " "))
return string.find(haystack, query, 1, true) ~= nil
end
local function findingMatchesTab(finding)
if currentTab == "ALL" then
return true
elseif currentTab == "RISKS" then
return finding.risk == true
end
return finding.category == currentTab
end
local function updateTabTexts()
local counts = {}
for _, category in ipairs(CATEGORY_ORDER) do
counts[category] = 0
end
counts.ALL = #findings
for _, finding in ipairs(findings) do
if finding.risk then
counts.RISKS = counts.RISKS + 1
end
if counts[finding.category] ~= nil then
counts[finding.category] = counts[finding.category] + 1
end
end
for category, button in pairs(tabButtons) do
button.Text = string.format("%-15s %d", CATEGORY_LABEL[category] or category, counts[category] or 0)
if category == currentTab then
button.BackgroundColor3 = COLORS.selected
button.TextColor3 = COLORS.text
else
button.BackgroundColor3 = COLORS.panel2
button.TextColor3 = COLORS.dim
end
end
end
local function updateSummary()
summaryLabel.Text = string.format(
"Scanned: %d\nRemotes: %d Code: %d\nState: %d attrs / %d values\nInteractions: %d\nPhysics assemblies: %d\nAssets: %d\nReview: %d High: %d Critical: %d%s",
stats.instances,
stats.remotes,
stats.code,
stats.attributes,
stats.values,
stats.interactions,
stats.assemblies,
stats.assets,
stats.risks,
stats.high,
stats.critical,
stats.truncated and "\n⚠ Scan truncated by limit" or ""
)
end
local function findingDetails(finding)
if not finding then
return "Select a finding to inspect it.\n\nSeverity is review priority, not proof of an exploit."
end
local lines = {
"SEVERITY: " .. finding.severity,
"CATEGORY: " .. finding.category,
"TYPE: " .. finding.className,
"TITLE: " .. finding.title,
"",
"PATH:",
finding.path,
}
if finding.key ~= "" then
table.insert(lines, "")
table.insert(lines, "KEY:")
table.insert(lines, finding.key)
end
if finding.value ~= "" then
table.insert(lines, "")
table.insert(lines, "VALUE:")
table.insert(lines, finding.value)
end
if finding.note ~= "" then
table.insert(lines, "")
table.insert(lines, "WHY IT MATTERS:")
table.insert(lines, finding.note)
end
table.insert(lines, "")
table.insert(lines, "RISK FLAG: " .. tostring(finding.risk))
return table.concat(lines, "\n")
end
local function selectFinding(finding)
selectedFinding = finding
detailsText.Text = findingDetails(finding)
detailsText.TextColor3 = finding and (SEVERITY_COLOR[finding.severity] or COLORS.text) or COLORS.text
detailsBody.CanvasPosition = Vector2.new(0, 0)
end
local function renderVirtualRows()
if closed then
return
end
local count = #filteredFindings
emptyLabel.Visible = count == 0
listFrame.CanvasSize = UDim2.fromOffset(0, count * CONFIG.ROW_HEIGHT)
local firstIndex = math.floor(listFrame.CanvasPosition.Y / CONFIG.ROW_HEIGHT) + 1
for poolIndex, row in ipairs(rowPool) do
local dataIndex = firstIndex + poolIndex - 1
local finding = filteredFindings[dataIndex]
if finding then
row.Visible = true
row.Position = UDim2.fromOffset(0, (dataIndex - 1) * CONFIG.ROW_HEIGHT)
row.Size = UDim2.new(1, -6, 0, CONFIG.ROW_HEIGHT - 1)
rowIndexMap[row] = dataIndex
local prefix = string.format("[%s] [%s] ", finding.severity, finding.category)
local keyValue = finding.key ~= "" and (" :: " .. finding.key) or ""
local value = finding.value ~= "" and (" = " .. finding.value) or ""
row.Text = prefix .. finding.path .. keyValue .. value
row.TextColor3 = SEVERITY_COLOR[finding.severity] or COLORS.text
row.BackgroundColor3 = (finding == selectedFinding) and COLORS.selected or COLORS.panel
else
row.Visible = false
rowIndexMap[row] = nil
end
end
end
local function applyFilters(resetScroll)
if closed then
return
end
local query = lower(searchBox.Text)
local severityMode = SEVERITY_MODES[severityModeIndex]
local output = {}
for _, finding in ipairs(findings) do
if findingMatchesTab(finding)
and finding.rank >= severityMode.minRank
and findingMatchesSearch(finding, query)
then
table.insert(output, finding)
end
end
filteredFindings = output
if resetScroll then
listFrame.CanvasPosition = Vector2.new(0, 0)
end
listFooter.Text = string.format(
"%d results | %s | %s%s",
#filteredFindings,
CATEGORY_LABEL[currentTab] or currentTab,
severityMode.label,
scanning and " | SCANNING" or ""
)
updateTabTexts()
updateSummary()
renderVirtualRows()
end
for order, category in ipairs(CATEGORY_ORDER) do
local button = new("TextButton", {
Name = "tab_" .. string.lower(category),
Size = UDim2.new(1, 0, 0, 31),
BackgroundColor3 = COLORS.panel2,
BorderSizePixel = 0,
Text = CATEGORY_LABEL[category] or category,
TextColor3 = COLORS.dim,
Font = Enum.Font.Code,
TextSize = 11,
TextXAlignment = Enum.TextXAlignment.Left,
AutoButtonColor = false,
LayoutOrder = order,
ZIndex = 30,
}, tabsContainer)
addCorner(button, 7)
new("UIPadding", {PaddingLeft = UDim.new(0, 9)}, button)
tabButtons[category] = button
trackConnection(button.Activated:Connect(function()
currentTab = category
applyFilters(true)
end))
end
for i = 1, CONFIG.ROW_POOL_SIZE do
local row = new("TextButton", {
Name = "row_" .. tostring(i),
Position = UDim2.fromOffset(0, 0),
Size = UDim2.new(1, -6, 0, CONFIG.ROW_HEIGHT - 1),
BackgroundColor3 = COLORS.panel,
BorderSizePixel = 0,
Text = "",
TextColor3 = COLORS.text,
Font = Enum.Font.Code,
TextSize = 12,
TextXAlignment = Enum.TextXAlignment.Left,
TextTruncate = Enum.TextTruncate.AtEnd,
AutoButtonColor = false,
Visible = false,
ZIndex = 27,
}, listFrame)
new("UIPadding", {PaddingLeft = UDim.new(0, 8), PaddingRight = UDim.new(0, 8)}, row)
table.insert(rowPool, row)
trackConnection(row.Activated:Connect(function()
local index = rowIndexMap[row]
if index then
selectFinding(filteredFindings[index])
renderVirtualRows()
end
end))
end
trackConnection(listFrame:GetPropertyChangedSignal("CanvasPosition"):Connect(renderVirtualRows))
trackConnection(searchBox:GetPropertyChangedSignal("Text"):Connect(function()
applyFilters(true)
end))
trackConnection(severityButton.Activated:Connect(function()
severityModeIndex = severityModeIndex + 1
if severityModeIndex > #SEVERITY_MODES then
severityModeIndex = 1
end
severityButton.Text = "Severity: " .. SEVERITY_MODES[severityModeIndex].label
applyFilters(true)
end))
-- ============================================================
-- Scan
-- ============================================================
local function clearResults()
findings = {}
filteredFindings = {}
findingKeys = {}
seenAssemblies = {}
seenAssets = {}
selectedFinding = nil
resetStats()
selectFinding(nil)
liveDirty = false
end
local function addSystemNotes()
local streaming = safeGet(workspace, "StreamingEnabled")
local placeInfo = string.format(
"PlaceId=%s | GameId=%s | JobId=%s | StreamingEnabled=%s",
tostring(game.PlaceId),
tostring(game.GameId),
tostring(game.JobId),
tostring(streaming)
)
addFinding(
"SYSTEM",
"INFO",
"Session",
nil,
"runtime",
placeInfo,
"Audit is read-only and only covers client-visible runtime state.",
false
)
if streaming == true then
addFinding(
"SYSTEM",
"MEDIUM",
"Workspace streaming is enabled",
nil,
"StreamingEnabled",
true,
"Initial Workspace scan only sees content currently streamed to this client. Live watch records new descendants as they stream in; move through the map or rescan from different regions for broader coverage.",
true
)
end
addFinding(
"SYSTEM",
"INFO",
"Threat model",
nil,
"assumption",
"Compromised client",
"Treat replicated code/data as readable and client-originated state/events as untrusted. This tool does not require or invoke ModuleScripts because require() could execute side effects.",
false
)
end
local function collectQueue(generation)
local queue = {}
local seen = {}
for _, root in ipairs(scanRoots) do
if closed or generation ~= scanGeneration then
return nil
end
if not seen[root] then
seen[root] = true
table.insert(queue, root)
end
local ok, descendants = pcall(function()
return root:GetDescendants()
end)
if ok then
for _, object in ipairs(descendants) do
if not seen[object] then
seen[object] = true
table.insert(queue, object)
if #queue >= CONFIG.MAX_INSTANCES then
stats.truncated = true
return queue
end
end
end
end
task.wait()
end
return queue
end
local function sortFindings()
table.sort(findings, function(a, b)
if a.rank ~= b.rank then
return a.rank > b.rank
end
if a.category ~= b.category then
return a.category < b.category
end
if a.path ~= b.path then
return a.path < b.path
end
return a.title < b.title
end)
end
local function startScan()
if closed then
return
end
scanGeneration = scanGeneration + 1
local generation = scanGeneration
scanning = true
rescanButton.Text = "SCANNING…"
headerStatus.Text = "building scan queue…"
clearResults()
addSystemNotes()
applyFilters(true)
task.spawn(function()
local queue = collectQueue(generation)
if not queue or closed or generation ~= scanGeneration then
return
end
local total = #queue
headerStatus.Text = string.format("0 / %d", total)
for index, object in ipairs(queue) do
if closed or generation ~= scanGeneration then
return
end
inspectInstance(object)
if index % CONFIG.SCAN_YIELD_EVERY == 0 then
headerStatus.Text = string.format("%d / %d", index, total)
updateSummary()
listFooter.Text = string.format("Scanning %d / %d…", index, total)
task.wait()
end
end
if closed or generation ~= scanGeneration then
return
end
sortFindings()
scanning = false
rescanButton.Text = "RESCAN"
headerStatus.Text = string.format("complete | %d findings", #findings)
applyFilters(true)
end)
end
trackConnection(rescanButton.Activated:Connect(startScan))
-- Live-watch catches newly replicated or newly streamed-in instances after the snapshot.
trackConnection(game.DescendantAdded:Connect(function(object)
if closed or scanning then
return
end
if isInScope(object) then
inspectInstance(object)
liveDirty = true
end
end))
trackConnection(RunService.Heartbeat:Connect(function()
if closed or scanning or not liveDirty then
return
end
local now = os.clock()
if now - lastLiveRefresh >= CONFIG.LIVE_REFRESH_INTERVAL then
lastLiveRefresh = now
liveDirty = false
applyFilters(false)
end
end))
-- ============================================================
-- Print/export helpers (normal Roblox output only)
-- ============================================================
local function findingToLine(finding)
local parts = {
"[" .. finding.severity .. "]",
"[" .. finding.category .. "]",
finding.path,
}
if finding.key ~= "" then
table.insert(parts, ":: " .. finding.key)
end
if finding.value ~= "" then
table.insert(parts, "= " .. finding.value)
end
return table.concat(parts, " ")
end
trackConnection(printButton.Activated:Connect(function()
print(string.format("=== CLIENT EXPOSURE DEBUGGER v%s | FILTERED EXPORT ===", VERSION))
print(string.format("Results: %d | Tab: %s | Severity: %s", #filteredFindings, currentTab, SEVERITY_MODES[severityModeIndex].label))
local limit = math.min(#filteredFindings, CONFIG.PRINT_LIMIT)
for i = 1, limit do
print(findingToLine(filteredFindings[i]))
end
if #filteredFindings > limit then
warn(string.format("Export truncated: printed %d of %d results.", limit, #filteredFindings))
end
end))
trackConnection(selectedPrintButton.Activated:Connect(function()
if selectedFinding then
print("=== CLIENT EXPOSURE DEBUGGER | SELECTED ===")
print(findingDetails(selectedFinding))
else
warn("[Client Exposure Debugger] No finding selected.")
end
end))
-- Kick off initial scan after GUI is fully parented/renderable.
task.defer(startScan)