Is there a way to prevent SQL injection style exploits with this?

Hi!

I wrote a custom programming language for my game so people could make and code their own games within my game.
I tested every way I could think of to run unregulated Luau code, and a large number of the ways (8/18) allow an exploit to run code.

  1. What do you want to achieve? How can I prevent this from happening?

  2. What is the issue? I said what it was above.

  3. What solutions have you tried so far? I haven’t been able to come up with anything.

Here’s the code.
It is a little messy. Sorry.

parser:

local parser = {}

local namespaces = {
	["echo"] = [[print( --=/?&4939769926_TEXT&/=-- ) ]];--vuln
	["--"] = [[-- --=/?&4939769926_TEXT&/=-- ]];
	["var"] = [[local --=/?&4939769926_NAME&/=-- = --=/?&4939769926_VALUE&/=-- ]];--vuln
	["close"] = [[end]];
	["setProperty"] = [[internal.setPropertyInternalFunction( --=/?&4939769926_ARGS&/=-- )]];--vuln
	["destroy"] = [[internal.destroyInternalFunction( --=/?&4939769926_ARGS&/=-- )]];--vuln
	["instantiate"] = [[internal.createInstanceInternalFunction( --=/?&4939769926_ARGS&/=-- )]];--vuln
	["if"] = [[if --=/?&4939769926_ONE&/=-- --=/?&4939769926_OPER&/=-- --=/?&4939769926_TWO&/=-- then]];
	["elif"] = [[elseif --=/?&4939769926_ONE&/=-- --=/?&4939769926_OPER&/=-- --=/?&4939769926_TWO&/=-- then]];
	["else"] = [[else]];
	["func"] = [[function --=/?&4939769926_NAME&/=-- ( --=/?&4939769926_ARGS&/=-- )]];
	["runfunc"] = [[--=/?&4939769926_NAME&/=-- ( --=/?&4939769926_ARGS&/=-- )]];--vuln
	["while"] = [[while --=/?&4939769926_PARAM&/=-- do]];
	["forpairs"] = [[for --=/?&4939769926_INDEX&/=-- ,--=/?&4939769926_OBJECT&/=-- in pairs ( --=/?&4939769926_TABLE&/=-- ) do]];
	["for"] = [[for --=/?&4939769926_INDEXVAR&/=--=--=/?&4939769926_START&/=-- ,--=/?&4939769926_MAX&/=-- ,--=/?&4939769926_INC&/=-- do]];
	["getchild"] = [[--=/?&4939769926_ARG1&/=-- = internal.getChildrenInternalFunction( --=/?&4939769926_ARG2&/=-- )]];--vuln
	["wait"] = [[wait( --=/?&4939769926_TIME&/=-- )]];--vuln
	["loadchar"] = [[internal.loadCharacterInternalFunction( --=/?&4939769926_ARGS&/=-- )]];--vuln
}

local datatypes = {
	["null"] = "nil";
}

local operands = {
	["is"] = "==";
	["isnt"] = "~=";
	["gthan"] = ">";
	["lthan"] = "<";
	["gthanoreq"] = ">=";
	["lthanoreq"] = "<=";
}
--How does this parser work?

--It essentially translates the code here to Lua. Thats it.

function parser:parse(code)
	--Remove all spaces from code, if they aren't between quotes
	code = code:gsub("[\n\r]", " ")
	local between = false
	
	local newCode = ""
	
	for _,char in pairs(string.split(code,"")) do
		if char == '"' then
			if between == true then
				between = false
			else
				between = true
			end
		end
		
		if between == false then
			newCode = newCode..char:gsub("%s+","")
		else
			newCode = newCode..char
		end
	end
	
	code = newCode
	--Split all of the code by the ; symbol.
	local lines = string.split(code,";")
	
	local emptyScript = [[--Lang.
--By jahoobas in 2025

--Required Dependencies
local lang = game:GetService("ReplicatedStorage"):WaitForChild("Lang")
local internal = require(lang:WaitForChild("internal"))

--START SCRIPT--
]]
	
	for _,line in pairs(lines) do
		local newline = ""
		--1. split by |
		local one = string.split(line,"|")
		
		--2. translate key word to Lua
		--Check if it has anything else
		if #one > 1 then
			if namespaces[one[1]]:match("%-%-=/%?%&4939769926_TEXT%&/=%-%- ") then
				local oneinlua = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_TEXT%&/=%-%-", one[2])
				newline = newline..oneinlua
			elseif namespaces[one[1]]:match("%-%-=/%?%&4939769926_NAME%&/=%-%- ") then
				local two = string.split(one[2],":")
				
				local pre = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_NAME%&/=%-%-", two[1])
				
				--check if it has a value
				if namespaces[one[1]]:match("%-%-=/%?%&4939769926_VALUE%&/=%-%- ") then
					local isDT = false
					for _,val in pairs(datatypes) do
						if val == datatypes[two[2]] then
							isDT = true
						end
					end
					
					if not isDT then
						newline = newline..pre:gsub("%-%-=/%?%&4939769926_VALUE%&/=%-%-", two[2])
					else
						newline = newline..pre:gsub("%-%-=/%?%&4939769926_VALUE%&/=%-%-", datatypes[two[2]])
					end
				elseif namespaces[one[1]]:match("%-%-=/%?%&4939769926_ARGS%&/=%-%- ") then 
					newline = newline..pre:gsub("%-%-=/%?%&4939769926_ARGS%&/=%-%-", two[2])
				end
			elseif namespaces[one[1]]:match("%-%-=/%?%&4939769926_ARGS%&/=%-%- ") and one[1] ~= "getchild" then
				local oneinlua = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_ARGS%&/=%-%-", one[2])
				newline = newline..oneinlua
			elseif namespaces[one[1]]:match("%-%-=/%?%&4939769926_ONE%&/=%-%- ") then
				--Is an if statement
				local two = string.split(one[2],"/")

				local pre = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_ONE%&/=%-%-", two[1])

				local pre2 = pre:gsub("%-%-=/%?%&4939769926_OPER%&/=%-%-", operands[two[2]])

				newline = newline..pre2:gsub("%-%-=/%?%&4939769926_TWO%&/=%-%-", two[3])
			elseif namespaces[one[1]]:match("%-%-=/%?%&4939769926_INDEX%&/=%-%- ") then
				--Is an if statement
				local two = string.split(one[2],",")

				local pre = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_INDEX%&/=%-%-", two[1])
				local pre2 = pre:gsub("%-%-=/%?%&4939769926_OBJECT%&/=%-%-", two[2])

				newline = newline..pre2:gsub("%-%-=/%?%&4939769926_TABLE%&/=%-%-", two[3])
			elseif namespaces[one[1]]:match("%-%-=/%?%&4939769926_START%&/=%-%- ") then
				--Is an if statement
				local two = string.split(one[2],",")

				local pre = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_MAX%&/=%-%-", two[3])

				local pre2 = pre:gsub("%-%-=/%?%&4939769926_START%&/=%-%-", two[2])

				local pre3 = pre2:gsub("%-%-=/%?%&4939769926_INDEXVAR%&/=%-%-", two[1])

				newline = newline..pre3:gsub("%-%-=/%?%&4939769926_INC%&/=%-%-", two[4])
			elseif namespaces[one[1]]:match("%-%-=/%?%&4939769926_TIME%&/=%-%- ") then
				local oneinlua = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_TIME%&/=%-%-", one[2])
				newline = newline..oneinlua
			elseif namespaces[one[1]]:match("%-%-=/%?%&4939769926_PARAM%&/=%-%- ") then
				local oneinlua = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_PARAM%&/=%-%-", one[2])
				newline = newline..oneinlua
			elseif one[1] == "getchild" then
				--Specific to this function
				local two = string.split(one[2],",")

				local pre = namespaces[one[1]]:gsub("%-%-=/%?%&4939769926_ARG1%&/=%-%-", two[1])

				local pre2 = pre:gsub("%-%-=/%?%&4939769926_ARG2%&/=%-%-", two[2])
				
				newline = newline..pre2
			end
		else
			if one[1] ~= "" then
				--Is just an alone keyword
				newline = namespaces[one[1]]
			end
		end
		
		emptyScript = emptyScript.."\n"..newline
	end
	
	emptyScript = emptyScript.."\n\n --END SCRIPT--\n\n--By jahoobas in 2025"
	
	return emptyScript
end

return parser

internal:

local internal = {}

function internal.setPropertyInternalFunction(obj : Instance, prop, val)
	local disallowed = {
		obj:FindFirstAncestor("ReplicatedStorage") or obj == game:GetService("ReplicatedStorage");
		obj:FindFirstAncestor("ReplicatedFirst") or obj == game:GetService("ReplicatedFirst");
		obj:FindFirstAncestor("ServerScriptService") or obj == game:GetService("ServerScriptService");
		obj:FindFirstAncestor("ServerStorage") or obj == game:GetService("ServerStorage");
		obj:FindFirstAncestor("StarterPlayer") or obj == game:GetService("StarterPlayer");
		obj:FindFirstAncestor("TextChatService") or obj == game:GetService("TextChatService");
	} 
	--Regulate what we're changing
	local allowed = true
	
	for i,e in pairs(disallowed) do
		if e then
			allowed = false
		end
	end
	
	if allowed then
		obj[prop] = val
	else
		error("Fatal Error: function setProperty; Access is denied.")
	end
end

function internal.destroyInternalFunction(obj : Instance)
	local disallowed = {
		obj:FindFirstAncestor("ReplicatedStorage") or obj == game:GetService("ReplicatedStorage");
		obj:FindFirstAncestor("ReplicatedFirst") or obj == game:GetService("ReplicatedFirst");
		obj:FindFirstAncestor("ServerScriptService") or obj == game:GetService("ServerScriptService");
		obj:FindFirstAncestor("ServerStorage") or obj == game:GetService("ServerStorage");
		obj:FindFirstAncestor("StarterPlayer") or obj == game:GetService("StarterPlayer");
		obj:FindFirstAncestor("TextChatService") or obj == game:GetService("TextChatService");
	} 
	--Regulate what we're destroying
	local allowed = true

	for i,e in pairs(disallowed) do
		if e then
			allowed = false
		end
	end

	if allowed then
		obj:Destroy()
	else
		error("Fatal Error: function destroy; Access is denied.")
	end
end

function internal.createInstanceInternalFunction(parent,className)
	local disallowed = {
		parent:FindFirstAncestor("ReplicatedStorage") or parent == game:GetService("ReplicatedStorage");
		parent:FindFirstAncestor("ReplicatedFirst") or parent == game:GetService("ReplicatedFirst");
		parent:FindFirstAncestor("ServerScriptService") or parent == game:GetService("ServerScriptService");
		parent:FindFirstAncestor("ServerStorage") or parent == game:GetService("ServerStorage");
		parent:FindFirstAncestor("StarterPlayer") or parent == game:GetService("StarterPlayer");
		parent:FindFirstAncestor("TextChatService") or parent == game:GetService("TextChatService");
	} 
	--Regulate what we're destroying
	local allowed = true

	for i,e in pairs(disallowed) do
		if e then
			allowed = false
		end
	end

	if allowed then
		local new = Instance.new(className)
		new.Parent = parent

		return new
	else
		error("Fatal Error: function instantiate; Access is denied.")
	end
end

function internal.getChildrenInternalFunction(parent)
	local disallowed = {
		parent:FindFirstAncestor("ReplicatedStorage") or parent == game:GetService("ReplicatedStorage");
		parent:FindFirstAncestor("ReplicatedFirst") or parent == game:GetService("ReplicatedFirst");
		parent:FindFirstAncestor("ServerScriptService") or parent == game:GetService("ServerScriptService");
		parent:FindFirstAncestor("ServerStorage") or parent == game:GetService("ServerStorage");
		parent:FindFirstAncestor("StarterPlayer") or parent == game:GetService("StarterPlayer");
		parent:FindFirstAncestor("TextChatService") or parent == game:GetService("TextChatService");
	} 
	--Regulate what we're destroying
	local allowed = true

	for i,e in pairs(disallowed) do
		if e then
			allowed = false
		end
	end

	if allowed then
		return parent:GetChildren()
	else
		error("Fatal Error: function getchild; Access is denied.")
	end
end

function internal.loadCharacterInternalFunction(player)
	local disallowed = {
		not player:IsA("Player")
	} 
	--Regulate what we're destroying
	local allowed = true

	for i,e in pairs(disallowed) do
		if e then
			allowed = false
		end
	end

	if allowed then
		return player:LoadCharacter()
	else
		error("Fatal Error: function loadchar; Argument 1 is not of class 'Player'.")
	end
end

return internal

mainmodule:

local lang = {}

function lang:exec(code)
	local translated = require(script.Parent:WaitForChild("parser")):parse(code)

	loadstring(translated)()
end

return lang

Thank you if you are able to help me solve this.

1 Like

You should take a look at the Script Capabilities Preview topic before writing a new programming language. It allows you to block things from being called/ran from within a script.

1 Like

I did not know this was a thing. Thank you!

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.