Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Security report : reference

Every check of the APIs Security Report, with what it looks for and how to fix it.

How a hook is read

The report reads the code of a hook, it never runs it, and it judges what the code does, not its name or its comments. A hook scopes rows when the person behind the request reaches the filter :

// Scopes : the id of the person goes into the filter.
const me = g.req.auth.authAMDB.customer_id;
g.req.query.find = { $and: [g.req.query.find || {}, { customer_id: me }] };

// Does not scope : it reads the person to decide something, then writes a filter of its own.
// Every person still reads the orders of every other person (the active ones).
if (g.req.auth.authAMDB.role !== 'admin') {
    g.req.query.find = { $and: [g.req.query.find || {}, { status: 'active' }] };
}

The report follows the id of the person through variables (me, scope), objects and helper functions, in all eight languages. A few more things decide what counts :

  • A hook runs when it is active (isActive true) ; one which does not say so does not run, and does not count.
  • A hook of an API (API level) runs before that API only : a hook of get all does not scope get by id, and the report lists the APIs it leaves open.
  • A hook which picks APIs by their id scopes the ones it picks : if (apiId === 'SCHEMA_GET_ALL') { … } leaves get by id, update by id … with the filter their caller sends, and the report lists them. A switch with a default: throw, or a refusal after the branches, refuses the APIs it does not handle : nothing is open there. TypeScript is read with its ifs, switches and early returns ; the other languages with their ifs.
  • The person has to keep the rows to their own : a filter which gives the person as one choice of an $or beside the filter of the caller ({ $or: [find, { customer_id: me }] }, any caller sends a filter every row matches), or puts the person under $ne, $nin, $not, $gt ..., lets the rows of everybody else through. The report reads it as no scoping and says why.
  • An API whose settings say NO_ACCESS answers nobody outside the admin panel : it is not looked at. A public one (IS_PUBLIC) asks for no token at all : its own finding says so. The access of an API is the one its settings say, else its collection's, else its database's.
  • A group whose active is false still grants : a request reads the groups of its API user by their names alone. The report counts its grants, and says it is switched off.

deep, find-join and master save reach nothing more : for a request of a person or an application, the rows of the related collection are read (and, for master save, changed) through its own APIs as that caller : with the group check of the caller, the pre and post hooks of that collection (its row scoping), and never in a collection whose settings say NO_ACCESS. Code which calls g.sys.db runs under its own rules : the hooks run when it passes skipHookRunning: false. A find carries no SQL of its own either : on the SQL instances every value of a query is a parameter of the statement.

What the report finds

Besides the code (see The code scan), these are the findings of the settings, by category :

Finding Severity What it means
Row gate
A collection not scoped to the caller (ROW_SCOPE_MISSING) critical when its owner column is sure, high or medium otherwise No active pre-hook puts the person into the filter : every person reads every row
The users table reachable (USERS_TABLE_EXPOSED) critical The users table of an auth provider is reachable by the groups and not limited to the row of each person : every account can be listed
A person can change their own account (USERS_TABLE_WRITABLE) high, medium without a groups column The users table is limited to the row of each person, but the groups may write it : a person changes their own groups ("*" is every group) or password
Rows of persons without a person token (ROW_SCOPE_NO_PERSON_TOKEN) high or medium The rows have an owner, but the APIs ask for no person token : nothing can scope them
No person token anywhere (NO_PERSON_TOKEN_ANYWHERE) high No API of the account asks for the token of a person
The scoping hook is off (ROW_SCOPE_HOOK_INACTIVE) high The hook which would scope the rows is switched off
Some APIs not scoped (ROW_SCOPE_PARTIAL) high The hook scopes some families of APIs, or some APIs (a hook of one API), and not the others
Aggregate / distinct bypass the scoping (ROW_SCOPE_BYPASS_APIS) high They have no place for the filter of the owner, and nothing refuses them
Some groups not scoped (ROW_SCOPE_GROUP_GATED) high The hook runs for some groups only (groupNames), other groups reach the collection too
A later hook undoes the scoping (ROW_SCOPE_UNDONE_BY_LATER_HOOK) high A hook which runs after the scoping one gives the filter a new value
Writes trust the owner sent (ROW_SCOPE_OWNER_NOT_STAMPED) medium Saves keep the owner the caller sent : a person creates rows for someone else
A weak merge (ROW_SCOPE_WEAK_MERGE) medium The owner is put next to the filter of the caller with a spread, or in its place, not with $and
Internal calls scoped too (ROW_SCOPE_NO_INTERNAL_GUARD) info Custom APIs, schedulers and events which carry no person are scoped too
Public access
A public collection or API (INSTANCE_API_PUBLIC) critical IS_PUBLIC : anyone on the internet reads, writes and deletes the rows
A public system API (SYSTEM_API_PUBLIC) critical for the sensitive ones, lower for the others IS_PUBLIC in the settings of a system API : anyone runs a query, reads the secrets, makes the server call an address ... (the API which gives tokens is public by design and not reported)
A public custom API (CUSTOM_API_PUBLIC) medium when it reads the databases, low otherwise The custom API answers without a token
API gate
Broad flags (GROUP_ALL_INSTANCES, GROUP_ALL_DATABASES, GROUP_ALL_COLLECTIONS, GROUP_ALL_INSTANCE_APIS, GROUP_ALL_SYSTEM_APIS, GROUP_ALL_CUSTOM_APIS, GROUP_ALL_WEB_SOCKET_EVENTS, GROUP_ALL_FIELDS) critical to low A group reaches everything of a kind, including what is added later
Sensitive system APIs granted (GROUP_DANGEROUS_SYSTEM_API) high Execute plain query, Get secret keys, the cache APIs, the indexes, Call external API, Is valid connection string
Bulk update / delete granted (GROUP_BULK_WRITE_APIS) medium update-many and remove-by-query, which change every row a filter matches
Auth providers
Passwords stored in clear (PROVIDER_PASSWORD_NOT_HASHED) critical The schema of the users table neither hashes nor encrypts the password column
Tokens of any application taken (SSO_AUDIENCE_NOT_CHECKED) high Google without clientId, Azure with an empty audience : a token made for another application (which any site can get from its own users) is taken as a login
Long lived tokens (PROVIDER_LONG_EXPIRY) medium A token lives more than 30 days
No groups column, no groups source, no condition (PROVIDER_NO_GROUPS_COLUMN, SSO_NO_GROUPS_SOURCE, PROVIDER_NO_CONDITION) low Every person gets the groups of the API user, or a disabled account still logs in
Hygiene
Unused groups and providers, API users without groups, Swagger docs on (GROUP_UNUSED, PROVIDER_UNUSED, API_USER_NO_GROUPS, API_USER_SWAGGER_ENABLED) info Loose ends which hide the real problems

A token of a DB token generator never carries the password of its person : it carries a fingerprint of it (made with the secret of the tokens, which never leaves the server), or nothing when the provider names a passwordChangedAtColumn, and a password which changes ends the tokens made before. There is nothing to find there.

Every language

A hook can be written in any language API Maker runs, and the report reads all of them the same way : a Python hook which sets g.req.query["find"], a Go hook which calls g.Req.Query.Set("find", ...) and a TypeScript hook which assigns g.req.query.find are the same scoping to it. The checks of the row gate (which identity is read, which families of APIs are filtered, whether the owner is merged with $and, stamped on writes, whether aggregate and distinct are refused, whether internal calls are let through) work in every language.

The fixes write code in the right language too. An API runs the code of one language on one runtime, its hooks included, so :

  • a new row scoping hook is written in the language of the hooks which already run for the collection, and the language can not be changed in the dialog (another one would be refused when it is saved) ;
  • when no hook runs for the collection yet, the language most of the code of the account is written in is proposed, and any of the eight can be chosen ;
  • a hook which is rewritten, or given the guard of the internal calls, stays in its own language.

The code scan

Settings tell who may call what. The code decides what an API then does, and it can leak data on its own : a custom API which returns the password column, a hook which takes the owner from the body, a query put together from what the caller sends. The scan of the code reads every piece of code of the account, in every language :

  • the custom APIs, and the hooks of the version in use of each,
  • the hooks of the instances, databases, collections and APIs, before and after,
  • the hooks of the system APIs,
  • the schedulers, the listeners of the events, the WebSocket events, the utility classes, the process initializers and the database migrations,
  • the functions of the auth providers.

Its findings are in the category Code. Each one names the piece of code, the line and the language, shows the lines, says why it matters and what to do, and links to the page where the code is edited. A custom API which answers without a token makes most findings one level more severe : anyone on the internet can call it.

Finding Severity What it means One click fix
A secret in the code (CODE_HARDCODED_SECRET) high ; critical for a private key, an AWS access key, a live key of Stripe ; medium for a test key A key, a password or a token is written in the code, so in git and every copy of it (the report shows it masked) the finding shows the code which reads it from Secret Management, in the language of the code
A secret returned (CODE_SECRET_RETURNED) critical What getSecret gave is part of the answer
Tokens returned (CODE_TOKEN_ECHOED) low g.req.auth or the headers are part of the answer
Sensitive values in the logs (CODE_SENSITIVE_LOGGED) medium A token, a password or a secret is printed to the logs
The users table read with its passwords (CODE_USERS_TABLE_UNSELECTED) high in a custom API, medium elsewhere A call reads the table of an auth provider without leaving its password column out adds a select which leaves the column out
The caller writes the filter (CODE_CALLER_CONTROLS_FILTER) high The filter (or the whole query) of a call of g.sys.db comes from the request as it is
Rows of persons read unscoped (CODE_OWNED_ROWS_UNSCOPED) high A table whose rows belong to persons is read with no filter on its owner column
A row by an id the caller sends (CODE_ID_FROM_REQUEST) high for writes, medium for reads The id comes from the request and the code never reads who the caller is
The body written as it is (CODE_MASS_ASSIGNMENT) medium Every column the caller sends is written : the owner, a role, a price
Error stacks returned (CODE_ERROR_DETAILS_RETURNED) low A stack trace is part of the answer
SQL put together from the request (CODE_SQL_INJECTION) critical A value of the request is pasted into the text of a statement
Commands run (CODE_COMMAND_EXECUTION) critical with a value of the request, medium otherwise exec, system, subprocess, Runtime.exec ...
Code made at run time (CODE_DYNAMIC_EVAL) critical with a value of the request, medium otherwise eval, new Function ...
An address the caller chooses (CODE_SSRF) high An outgoing call goes where the request says : the metadata of the cloud, the databases, the other services
A file the caller names (CODE_PATH_TRAVERSAL) high A path of the request reaches a file of the machine
TLS checks off (CODE_TLS_VERIFY_DISABLED) high rejectUnauthorized: false, verify=False, InsecureSkipVerify: true ... turns the checks back on
Weak password hashes (CODE_WEAK_PASSWORD_HASH) medium A password is hashed with MD5 or SHA-1
Guessable random tokens (CODE_INSECURE_RANDOM) medium A token or a code is made with Math.random, random.random, rand ...
A hook which changes who the caller is (CODE_HOOK_FORGES_IDENTITY) critical g.req.auth or g.req.isApiRequestFromUser is written by a hook
A caller who turns the scoping off (CODE_HOOK_CALLER_BYPASS) high A hook returns before it scopes the rows when the request holds a header, a parameter or a field

The key of a finding names the rule, the piece of code and what was found, not the line : a finding stays the same finding (and a skip stays a skip) when lines are added above it.