A Current Explanation of Normal Identities and Security Tags

If you’ve been around Roblox long enough, chances are that you’ve come across members of the API that you can’t access or use and seen phrases like ‘LocalUserSecurity’ or ‘PluginSecurity’ on the wiki. There’s not much documentation for what these things are or how they function, so I thought I would put together a thread about it. If you’re a returning developer and you have working knowledge of normal identities and security tags, this may be a review, but hopefully it will answer some questions or clear up problems.

What is a normal identity?

Every script run in Roblox, whether it be a plugin, a LocalScript, or a CoreScript, all have a level of access that’s assigned to them that determines what they can do. This level is called a normal identity. These identities exist to prevent people from maliciously misusing sensitive APIs and changing things they shouldn’t. Normal identities on Roblox can be gotten by calling the global function printidentity in any Lua environment. This prints a number version of these identities that can be used for reference.

Here’s a table of them by source:

Source Identity
Script 2
LocalScript 2
ModuleScript Varies*
CoreScript 3
Command Line 4
Run Script 4
Plugin 5

*ModuleScripts run at the identity they are required at

From the above table you may expect that plugins have the highest permissions available as their identity is the highest. That is not the case. Of those things, CoreScripts actually have the highest overall permissions. The number associated with identities is arbitrary and has no meaning beyond what it’s associated with.

Why do I care?

As I said earlier, each normal identity determines what Roblox API and properties can be accessed and changed. If you’re coding a normal game, there’s a good chance this won’t matter to you unless you’re on the developer hub or messing around with studio’s settings. If you’re a veteran developer and are interested in knowledge for it’s own sake or in making plugins (or even messing with the CoreScripts!) it’s probably important to know what can do what though. There’s also a chance you’ll run into a weird error eventually in your time as a developer and this will help you diagnose and solve this error better.

This error is, simply, The current identity (X) cannot Y (lacking permission Z), where X is the normal identity of the script executing the code, Y is a description of what’s being attempted, and Z is a number that indicates what permission is necessary to access these things. Rather than explaining what’s wrong with this error or why it may be confusing, I’ll provide some examples.

Using the code game.Players.Dekkonot.Name = "foo" in a Plugin

Using the code game.Players.Dekkonot.Name = "foo" in the Command Line

Using the code: game:GetService("CSGDictionaryService"):GetChildren() in the Command Line

Using the code game:GetService("ScriptContext"):AddCoreScriptLocal("foo", workspace) in the Command Line

If you think it looks like the permission numbers are entirely seperate from the actual normal identities, you’d be right. This is where security tags come in.

Security Tags

Without overcomplicating things or going too in-depth, security tags are flags on API members and Instances that indicate what permissions are needed to access them. After doing some analysis on the API dump generated by studio, it can be found that 5 security tags exist, not counting those that have no security: LocalUserSecurity, PluginSecurity, RobloxScriptSecurity, NotAccessibleSecurity, RobloxSecurity

If some testing is done, it can be found out that each normal identity can access API members with specific flags on them. A list of them is here:

Normal Identity Accessible Security Tags
4 LocalUserSecurity, PluginSecurity
5 LocalUserSecurity, PluginSecurity
3 LocalUserSecurity, PluginSecurity, RobloxScriptSecurity

Noticably absent from this table is RobloxSecurity and NotAccessibleSecurity. NotAccessibleSecurity is self-explanatory as it means that the API member literally can’t be accessed by scripts, but RobloxSecurity is a bit more complicated. I’ll get back to that at the end of the post.

In addition to having string names like ‘LocalUserSecurity’, each security tag appears to have a number associated with it that shows up in errors. After some testing it looks as though the relation is this:

Security Tag Permission Level
LocalUserSecurity 3
PluginSecurity 1
RobloxScriptSecurity 5,1
RobloxSecurity 6
NotAccessibleSecurity ???

As you can see by the ‘???’, I cannot actually confirm what, if any, permission level NotAccessibleSecurity has. This will probably never come up because it only occurs on 3 API members (that it’s listed on anyways). I also have no idea why RobloxScriptSecurity has two permissions associated with it.

This explains what the permission numbers mean, but it fails to account for a few cases. Namely, it fails to explain the locking of specific instances (CSGDictionaryService, AnalyticsService, etc.) and the locking of Player’s Name properties. The former however is easily explained with the RobloxLocked property. This property simply sets whether an Instance (and its descendants) can be accessed. There appear to be two levels of RobloxLocked, though that is purely based on observation and should not be taken as fact: those set to prevent normal scripts from accessing things and those set to prevent all access to something. Examples of the first type include the CoreGui, which is inaccessible to normal scripts but is accessible to everything else. Examples of the second include the CSGDictionaryService, which is inaccessible to all levels.

The latter case, Player names being locked, appears to be hardcoded in, as it generates an error that is unique from other restricted properties. A comparison can be seen in the screenshots earlier.


As promised, here’s an explanation of the RobloxSecurity security tag, as it appears to be an anomaly in that no threads can access those API members regardless of the identity. That’s because members with this security tag are meant soley for internal use, and are thus heavily restricted. According to the most recent API dump, there are exactly 6 members of the API with this security tag. Of them, only 2 are actually accessible (Player::GetGameSessionID and Player::SetUnder13). I did not verify what identity is required to access members tagged with this security, but if the past is anything to go on, it would require an unrestricted thread, which means it had an undefined context identity. This generally comes in the form of 7 (this is where the infamous term ‘level 7’ comes from when talking about exploits), but it could be any undefined number (1, 6, 0, etc.). It’s recommended you don’t think too much about this because these API members have no practical use to developers and in fact cannot be used by them anyways.

In Conclusion

Roblox’s normal identity system isn’t documented in an up-to-date manner anywhere, although it’s actually rather simple once you look into it. This thread was prompted because I realized how weird the errors were and how the identity numbers didn’t line up with the permission numbers. It is my hope that I’ve educated a few people on the subject as well as provided a more up to date explanation on the security and normal identitity systems for Roblox. If you have any questions, or think I’ve gotten something wrong please let me know, either in a reply to this thread or in a PM.

Additional Info Available Here

This script was used to test the normal identities and what security tags could be accessed by them. https://pastebin.com/X8K1tVuZ

To test CoreGuis, I simply used a CoreGui override. More information can be provided on this if someone asked.


Excellent explanation! There’s something I’d like a clarification on. For instance, a LocalScript can put a function inside _G and then this function could be called by a thread of any identity, such as the command bar. This function would then run under the caller’s identity. Correct me if I’m wrong, but I believe _G and other shared tables are shared among all identities. So this would mean that code in LocalScripts does not necessarily run under identity 2.

1 Like

_G is a distinct table for client and server, if a localscript defines a function in _G the server does not have access to it. I don’t believe the command bar can define something in _G that a regular script can access either.

Sorry, I wasn’t being specific. I meant the Studio command bar.

I am aware of that, but if you set anything in _G via the studio command bar and have a script/localscript try to access it, it returns nil.

This is incorrect to my knowledge. It’s not possible to transfer Lua objects such as tables or functions between identities directly. _G and shared are not shared between identities. BindableEvents and BindableFunctions “sanitize” their inputs: functions are removed, metatables are removed, and tables must match a more strict format as if they are being serialized to JSON.

I’m fairly sure that serious exploits have been developed in the past that made use of shared Lua environment to run code at a higher identity on the server. I think that was at a time when there was not a distinct split between identities and when you could transfer Lua objects between identities. This separation is intended as a layer of security.

1 Like

That’s good then. Thanks for the correction.

It’s annoying when a corescript errors and your custom error reporter tries to access the error and it errors because it’s 1 security level apart :upside_down_face:


Built in Plugins have an identity of 6, and expressionEval has an identity of 1.

Plugins have LocalUserSecurity?

This guide is actually outdated slightly (ironic given the title). I’ll update it when I have a chance but I can’t give any ETAs on that.


What’s expressionEval?

Sorry if it’s a stupid question, but Google couldn’t find anything and your post is the only search result on this forum.

Edit: Searching for “expression eval” (with a space) returns results about the property window and you being able to enter Lua expressions as values… is this what you mean?

This is outdated as well - undefined levels have no permissions. Level 7 exploits no longer exist. They are all level 6 or 3, and only some of them have the “unscriptable patch”. The “unscriptable patch” essentially allows you to access everything, even things as heavily guarded as UnionOperation.AssetId (which does exist, no matter what the api docs or error messages suggest), or things with NotAccessibleSecurity.

Not sure if saying this on the devforum is a great idea, but I do have access to one particular injector, which does not have this patch, only the ability to set its identity level arbitrarily, which is how I did my testing regarding the now-patched level 7.

don’t ban me roblox please

Speaking of identity 6, it is completely absent from your post. I believe that is now the level the command bar runs at (or 3 idk), but plugins might still run at 5 (missing LocalUserSecurity). Test it :stuck_out_tongue:


print(debug.traceback()) --> expressionEval:1
1 Like

A level 7 exploit is the same term as executing a localscript thread with the context identity of 6 or higher, higher will default to 6 I presume.

With exploits developing more and more every week we see more vulnerabilities being exposed within the Roblox Client and how easy it is to manipulate restrictions set in place e.g. for userdata values using metatable hooking.

Hopefully, we will see FE become FEIE one day!

Filtering Enabled Including Exploits :yum:

Cool thread none the less very well explained, some information needs to be updated though.

Level 7 literally does not exist anymore, and has no permissions. So, there is no higher, unless the exploit defines itself a brand new identity level.

As I said you can set the thread’s context to any level or higher which if goes to high will just display the number itself and default to the highest thread context.

Yes, it is no longer referred to as “level 7”, no exploits are really referred to as levels now, more what they can execute. However it is the same concept just differently phrased.

I don’t believe that’s correct. Certain exploits lets you change the thread identity level at will, and changing it to level 7, level 42, etc. defaults to no permissions. However, level 6 is pretty nice and level 3 is CoreScript permissions.

It does display the number itself but it defaults to zero permissions. I believe privileged functions define which identities (i.e. just numbers) can execute them, rather than the other way around. However, I’ve never reverse-engineered Roblox or any exploits.

Yeah, any exploit with a set_thread_identity or etc. function can do pretty much anything. Including automating CoreScript things or messing with RobloxLocked things.

That’s exactly what I said but if you change it to anything higher than the highest script level, it will default to the highest script level. Any context level above 7 does nothing.

6 is the current highest I believe but 7 can be used for a certain level that isn’t mentioned in the public api documentation I believe.

No it’s not.

No. The highest script level is 6. Changing it to 7 or 42 or 999 does not default to 6. Since 3 or 6 or any of the other current identity levels are the only ones that are defined, changing to some other number won’t magically give you the “highest script level”, which is 6. It’ll give you an undefined script level which is either you can’t access anything, or normal script level.

No, it defaults to the highest script level. I know this because I was curious so I read paid exploits api documentation.

Edit: In fact as of a recent community update, the level 7 term has officially been added back and is in use again.