You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Manual review of flags.ts and mod_test.ts. This library is the authorization primitive for UrsaMU — bugs here propagate into every permission check in the engine.
CRITICAL
[AuthBypass] flags.tscodes() — unknown flag appends literal "false" to output
The && short-circuit returns the boolean false when a flag is not found, and .reduce() coerces it to the string "false":
codes("admin unknownflag") returns "Afalse" rather than "A". Any caller that uses the returned code string for permission comparisons (e.g., checking character codes in a display or auth layer) will silently receive a corrupted result. Fix: filter out falsy values before reducing, or use this.exists(flag)?.code ?? "".
[PrivEsc] flags.tscheck() — + level expression grants access based on the entity's highest flag level, not the specific flag
this.lvl(list) returns the highest level among all flags the entity has. So check("wizard", "user+") passes — even though the entity does not have the user flag — because wizard.lvl >= user.lvl. This means possession of any single high-level flag satisfies every lower-tier + expression, regardless of which flags are actually on the entity.
HIGH
[DataLoss] flags.ts_add() — re-registering a flag silently drops add, remove, lock, and data callbacks
When an existing flag is updated via add(), the replacement object only carries {name, code, lvl}:
All add, remove, lock, and data fields from the original registration are silently lost. If a security hook was registered on a flag (e.g., remove: (data) => revokeAdminState(data)), re-calling flags.add(...) with an updated code or level removes the hook without any warning.
MEDIUM
[FalsyCoercion] flags.tslvl() and check() — || 0 coerces explicit lvl: 0 identically to undefined
The first line uses || 0, which treats lvl: 0 as falsy and substitutes 0 — same as when lvl is absent. A flag explicitly set to lvl: 0 is indistinguishable from one with no lvl property. Use ?? 0 (nullish coalescing) throughout to only substitute for null / undefined.
[DataMutation] flags.tsset() — mutates the caller's data object in place
set() performs delete data[item.slice(1)] and data[flag.name] = flag.data directly on the data argument. Callers likely do not expect their object to be modified; this makes the function unsafe to call with a shared or frozen object. The data parameter should be cloned at the start of the function (const result = { ...data }).
[BrokenDoc] flags.tscheck() — README example with spaces around | does not work
The README documents:
flags.check("admin user","admin | moderator")
But flagExpr.split(" ") splits this into three tokens: "admin", "|", "moderator". The "|" token matches flag.includes("|") → splits to ["", ""] → both are empty strings → compareFlag("", listArray) → always false. The OR check silently fails. The implementation requires no spaces: "admin|moderator". The test file already uses the correct no-space form; the README example should be corrected (or the parser should trim spaces around |).
LOW / INFORMATIONAL
[Inconsistency] flags.tsexists() — code lookup is case-sensitive, name lookup is case-insensitive
exists("admin") and exists("ADMIN") both find {name: "admin", code: "A"} by the name branch. But exists("a") and exists("A") behave differently for the code branch: only the exact-case "A" matches. This inconsistency can cause subtle bugs in callers that look up flags by code.
[PerfDuplication] flags.tscodes() — this.exists(flag) called twice per flag
Audit Findings
Manual review of
flags.tsandmod_test.ts. This library is the authorization primitive for UrsaMU — bugs here propagate into every permission check in the engine.CRITICAL
[AuthBypass]
flags.tscodes()— unknown flag appends literal"false"to outputThe
&&short-circuit returns the booleanfalsewhen a flag is not found, and.reduce()coerces it to the string"false":codes("admin unknownflag")returns"Afalse"rather than"A". Any caller that uses the returned code string for permission comparisons (e.g., checking character codes in a display or auth layer) will silently receive a corrupted result. Fix: filter out falsy values before reducing, or usethis.exists(flag)?.code ?? "".[PrivEsc]
flags.tscheck()—+level expression grants access based on the entity's highest flag level, not the specific flagthis.lvl(list)returns the highest level among all flags the entity has. Socheck("wizard", "user+")passes — even though the entity does not have theuserflag — becausewizard.lvl >= user.lvl. This means possession of any single high-level flag satisfies every lower-tier+expression, regardless of which flags are actually on the entity.HIGH
[DataLoss]
flags.ts_add()— re-registering a flag silently dropsadd,remove,lock, anddatacallbacksWhen an existing flag is updated via
add(), the replacement object only carries{name, code, lvl}:All
add,remove,lock, anddatafields from the original registration are silently lost. If a security hook was registered on a flag (e.g.,remove: (data) => revokeAdminState(data)), re-callingflags.add(...)with an updated code or level removes the hook without any warning.MEDIUM
[FalsyCoercion]
flags.tslvl()andcheck()—|| 0coerces explicitlvl: 0identically toundefinedThe first line uses
|| 0, which treatslvl: 0as falsy and substitutes0— same as whenlvlis absent. A flag explicitly set tolvl: 0is indistinguishable from one with nolvlproperty. Use?? 0(nullish coalescing) throughout to only substitute fornull/undefined.[DataMutation]
flags.tsset()— mutates the caller'sdataobject in placeset()performsdelete data[item.slice(1)]anddata[flag.name] = flag.datadirectly on thedataargument. Callers likely do not expect their object to be modified; this makes the function unsafe to call with a shared or frozen object. Thedataparameter should be cloned at the start of the function (const result = { ...data }).[BrokenDoc]
flags.tscheck()— README example with spaces around|does not workThe README documents:
But
flagExpr.split(" ")splits this into three tokens:"admin","|","moderator". The"|"token matchesflag.includes("|")→ splits to["", ""]→ both are empty strings →compareFlag("", listArray)→ alwaysfalse. The OR check silently fails. The implementation requires no spaces:"admin|moderator". The test file already uses the correct no-space form; the README example should be corrected (or the parser should trim spaces around|).LOW / INFORMATIONAL
[Inconsistency]
flags.tsexists()— code lookup is case-sensitive, name lookup is case-insensitiveexists("admin")andexists("ADMIN")both find{name: "admin", code: "A"}by the name branch. Butexists("a")andexists("A")behave differently for the code branch: only the exact-case"A"matches. This inconsistency can cause subtle bugs in callers that look up flags by code.[PerfDuplication]
flags.tscodes()—this.exists(flag)called twice per flagexists()iterates the flags array on each call. The result should be stored in a variable to avoid the double scan.[ZeroCoverage] The existing 5 tests cover only happy paths. The following cases are untested:
codes()with an unknown flag (returns"false"bug)check()with a+level expressioncheck()with!negation combined with|OR_add()re-registering a flag with callbacks (callback loss bug)set()with an empty string as theflagsargumentlvl()with a flag that has an explicitlvl: 0exists()case-sensitivity for code lookup