Skip to content

New Rule: Validate authentication in database query functions #151

Description

@karlhorky

Copied description from karlhorky/eslint-tricks#2


Examples of incorrect code for this rule:

// ❌ No sessionToken in first parameter
export const selectUserById = cache(async (id: User['id']) => {
  const [user] = await sql<User[]>`
    SELECT
      id,
      name
    FROM
      users
  `;

  return user;
});

// ❌ No Unauthenticated suffix
export const selectAnimals = cache(async () => {
  const animals = await sql<Animal[]>`
    SELECT
      *
    FROM
      animals
    ORDER BY
      id
  `;

  return animals;
});

Examples of correct code for this rule:

// ✅ sessionToken in first parameter and used in query
export const selectUserById = cache(async (sessionToken: Session['token'], id: User['id']) => {
  const [user] = await sql<User[]>`
    SELECT
      id,
      name
    FROM
      users
      INNER JOIN sessions ON (
        sessions.token = ${sessionToken}
        AND sessions.expiry_timestamp > now()
      )
  `;

  return user;
});

// ✅ Unauthenticated suffix
export const selectAnimalsUnauthenticated = cache(async () => {
  const animals = await sql<Animal[]>`
    SELECT
      *
    FROM
      animals
    ORDER BY
      id
  `;

  return animals;
});

ESLint-level validation to make sure that exported database query functions either use sessionToken or are marked ...Unauthenticated (also supports wrapping in React cache())

Version with no-restricted-syntax:

overrides: [
    {
      files: ['packages/database/queries/*.ts'],
      rules: {
        'no-restricted-syntax': [
          ...noRestrictedSyntaxOptions,
          // Enforce unambiguous exported database function patterns
          // (require either accepting a session token ("sessionToken")
          // as the first parameter or having a name ending with
          // "Unauthenticated")
          {
            selector:
              "ExportNamedDeclaration > FunctionDeclaration[id.name!=/Unauthenticated$/][params.0.name!='sessionToken'], ExportNamedDeclaration > VariableDeclaration[declarations.0.init.callee.name='cache'][declarations.0.id.name!=/Unauthenticated$/][declarations.0.init.arguments.0.params.0.name!='sessionToken']",
            message: `Ambiguous authentication of exported database query function - either pass \`sessionToken\` as the first parameter or name the function ending with \`Unauthenticated\`:

  function getUser(sessionToken: string, userId: number)
  const getUser = cache(async (sessionToken: string, userId: number) =>

  function getArticleCategoriesUnauthenticated()
  const getArticleCategoriesUnauthenticated = cache(async () =>

`,
          },
          // Enforce usage of session token ("sessionToken"
          // first parameter) within database functions
          {
            selector:
              "ExportNamedDeclaration > FunctionDeclaration[params.0.name='sessionToken'] > BlockStatement:not(:has([type='Identifier'][name='sessionToken'])), ExportNamedDeclaration > VariableDeclaration[declarations.0.init.callee.name='cache'][declarations.0.init.arguments.0.params.0.name='sessionToken'] > BlockStatement:not(:has([type='Identifier'][name='sessionToken']))",
            message:
              'Unused `sessionToken` parameter in database query function - use `sessionToken` in database queries to implement authentication and authorization',
          },
        ],
      },
    },
  ],

Examples in VS Code:

Screenshot 2023-06-10 at 15 04 34 Screenshot 2023-06-10 at 16 13 58

More examples:


MVP

First version done in:

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions