Skip to content
Report library
Purpose / Other

Better Auth Security Best Practices Skill Security Audit

What the author says it does (original text)

Configure rate limiting, manage auth secrets, set up CSRF protection, define trusted origins, secure sessions and cookies, encrypt OAuth tokens, track IP addresses, and implement audit logging for Better Auth. Use when users need to secure their auth setup, prevent brute force attacks, or harden a Better Auth deployment.

Independent security check

Security risks found

Files checked
1
Risks found
5
Could it run dangerous commands?Looks for programs run straight after downloading, remote control of your computer, and hidden commands.No risks found
Could it expose your files or keys?Looks for uploads of files containing passwords or keys, and keys written directly in the code.Risks found: 2
Medium risk

The cross-subdomain cookie example exposes session credentials to matching subdomains

Source references: 1
What we found

The example enables cookies for .example.com and explicitly includes session_token and session_data. Domain cookies are sent with matching subdomain requests; HttpOnly does not stop a compromised subdomain server from receiving cookies in those requests.

Why this matters

A subdomain that is not fully trusted or is taken over may receive session cookies, potentially exposing or enabling misuse of user sessions.

Legitimate use of this code

The stated cookie-scope consequence is technically possible, but the source presents this as an optional cross-subdomain authentication-sharing configuration and immediately says to enable it only when sharing is needed and every subdomain is trusted. It is therefore not a hidden exfiltration instruction, but a feature example with an important boundary warning. Residual risk depends on whether all current and future subdomains truly remain trusted; users can ask for explicit discussion of subdomain takeover and compromised-subdomain impact.

This assessment concerns the code and conditions shown, not proof that harm has occurred.
SKILL.md:203In the instructionsOpen original file
### Cross-Subdomain Cookies```tsadvanced: {  crossSubDomainCookies: {    enabled: true,    domain: ".example.com", // Note the leading dot    additionalCookies: ["session_token", "session_data"],  },}```Only enable if you need authentication sharing and trust all subdomains.
Medium risk

Audit examples collect personal data without defining log protection or retention

Source references: 2
What we found

The hooks send IP addresses, user agents, and both old and new email addresses to auditLog, but do not define where that function sends data, who can access it, how long it is retained, or whether it is minimized.

Why this matters

If logs go to a third-party service, a broadly accessible console, or long-term storage, identity and network information may be unnecessarily disclosed and increase breach and compliance impact.

The example passes IP address and User-Agent to an undefined `auditLog` on session creation and records both old and new email addresses on email changes. These are identifying or linkable personal data, while the visible guidance does not constrain log destination, access, redaction, or retention. If copied and `auditLog` persists or exports the data, privacy, breach, and compliance exposure may increase. Users can ask for data minimization, trusted-proxy handling, access controls, encryption, and deletion periods.

SKILL.md:271In the instructionsOpen original file
      create: {        after: async ({ data, ctx }) => {          await auditLog("session.created", {            userId: data.userId,            ip: ctx?.request?.headers.get("x-forwarded-for"),            userAgent: ctx?.request?.headers.get("user-agent"),          });        },
Show 1 other places
SKILL.md:287In the instructionsOpen original file
      update: {        after: async ({ data, oldData }) => {          if (oldData?.email !== data.email) {            await auditLog("user.email_changed", {              userId: data.id,              oldEmail: oldData?.email,              newEmail: data.email,            });          }        },
Could it delete files or keep running?Looks for broad file deletion, disk overwrites, and programs set to start automatically.No risks found
Could it bypass safety checks?Looks for skipped website security checks, excessive file access, or actions that skip your approval.Risks found: 3
Medium risk

Broad or dynamic trusted origins may accept attacker-controlled subdomains

Source references: 4
What we found

The examples allow wildcard subdomains and show request-dependent origin generation without demonstrating a tenant allowlist or ownership check. These origins are also used to validate callback and redirect URLs.

Why this matters

If an attacker can register, take over, or otherwise control a matching subdomain, it may pass origin checks and be accepted as an authentication callback or redirect destination.

This is configuration guidance, not automatically executed code, but the risk is plausible. The wildcard trusts every matching subdomain, while the dynamic example interpolates a request-derived tenant and only comments that it should be validated, without showing an allowlist or ownership check. Because the same trusted-origin set validates callback and redirect URLs, copying this with an attacker-controlled subdomain or tenant value could broaden accepted authentication redirects. Users can ask for strict allowlisting, normalization, and tenant-ownership validation.

SKILL.md:129In the instructionsOpen original file
```tstrustedOrigins: [  "*.example.com", // Matches any subdomain  "https://*.example.com", // Protocol-specific wildcard  "exp://192.168.*.*:*/*", // Custom schemes (e.g., Expo)]```
Show 3 other places
SKILL.md:139In the instructionsOpen original file
Compute trusted origins based on the request:```tstrustedOrigins: async (request) => {  // Validate against database, header, etc.  const tenant = getTenantFromRequest(request);  return [`https://${tenant}.myapp.com`];}```
SKILL.md:149In the instructionsOpen original file
Validates `callbackURL`, `redirectTo`, `errorCallbackURL`, `newUserCallbackURL`, and `origin` against trusted origins. Invalid URLs receive 403.
SKILL.md:142In the instructionsOpen original file
```tstrustedOrigins: async (request) => {  // Validate against database, header, etc.  const tenant = getTenantFromRequest(request);  return [`https://${tenant}.myapp.com`];}```
Medium risk

Using forwarded-IP headers directly may let clients bypass IP-based controls

Source references: 2
What we found

The configuration uses x-forwarded-for and x-real-ip for IP tracking and rate limiting. Clients can forge these headers unless the application is reachable only through a trusted proxy that removes and rewrites external values.

Why this matters

An attacker may rotate fake IP values to evade sign-in limits. Audit records may also attribute activity to incorrect IPs, making investigations unreliable.

The example explicitly uses two request-supplied forwarding headers for IP identification and says IP tracking supports rate limiting. It warns that a separate `trustedProxyHeaders` setting is only for trusted reverse proxies, but does not explain that direct access must be blocked, incoming values stripped, or only a trusted proxy-added address accepted. Without those deployment controls, clients may spoof or rotate header values, weakening IP-based limits and audit attribution. Users can ask for explicit trusted-proxy-chain and header-sanitization requirements.

SKILL.md:250In the instructionsOpen original file
export const auth = betterAuth({  advanced: {    ipAddress: {      ipAddressHeaders: ["x-forwarded-for", "x-real-ip"], // Headers to check      disableIpTracking: false, // Keep enabled for rate limiting    },  },});```
Show 1 other places
SKILL.md:260In the instructionsOpen original file
Set `ipv6Subnet` (128, 64, 48, 32; default 64) to group IPv6 addresses. Enable `trustedProxyHeaders: true` only if behind a trusted reverse proxy.
Medium risk

The mobile guidance permits skipping the OAuth state-cookie check

Source references: 2
What we found

The guide permits skipStateCookieCheck: true for mobile applications that cannot maintain cookies, without also requiring an equivalent one-time state check bound to the initiating device. PKCE protects authorization-code exchange but does not replace every request-correlation role of state.

Why this matters

If the check is only disabled, an attacker may have an easier path to login CSRF, incorrect account linking, or injecting another person's OAuth response into a victim's flow.

The source says OAuth uses PKCE and short-lived random state, then permits `skipStateCookieCheck: true` for mobile apps that cannot retain cookies. The visible guidance does not require an equivalent one-time mechanism bound to the initiating device or app instance. If this option removes the only local correlation check, login-flow confusion or login CSRF may become easier; PKCE serves a different authorization-code binding role. This is conditional advice, not evidence of an attack. Users can ask for a complete equivalent mobile validation design.

SKILL.md:219In the instructionsOpen original file
PKCE is automatic for all OAuth flows. State tokens are 32-char random strings expiring after 10 minutes.
Show 1 other places
SKILL.md:241In the instructionsOpen original file
Enable if storing OAuth tokens for API access on behalf of users. Use `skipStateCookieCheck: true` only for mobile apps that cannot maintain cookies.
Could it mislead the AI or hide text?Checks the skill instructions for requests to ignore you, influence the report, or hide text in invisible characters.No risks found
Could it change links or payment recipients without asking?Looks for forced referral or payment changes combined with instructions to hide the change.No risks found

Inside this skill

8 instruction sections

This Skill is a Better Auth security configuration guide with copyable TypeScript snippets; the shown behavior takes effect only if a user adds the snippets to an application.

View source
SKILL.md:10In the instructionsOpen original file
```tsimport { betterAuth } from "better-auth";export const auth = betterAuth({  secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env});```

The guide recommends production rate limiting and CSRF checks, with the authentication secret supplied through an environment variable.

View source
SKILL.md:18In the instructionsOpen original file
Better Auth looks for secrets in this order:1. `options.secret` in your config2. `BETTER_AUTH_SECRET` environment variable3. `AUTH_SECRET` environment variable### Secret Requirements- Rejects default/placeholder secrets in production- Warns if shorter than 32 characters or entropy below 120 bits- Generate: `openssl rand -base64 32`- Never commit secrets to version control
SKILL.md:100In the instructionsOpen original file
export const auth = betterAuth({  advanced: {    disableCSRFCheck: false, // Default: false (keep enabled)  },});```Only disable for testing or with an alternative CSRF mechanism.

The audit-hook examples pass user identifiers, IP addresses, user agents, and email-change details to an undefined auditLog implementation.

View source
SKILL.md:271In the instructionsOpen original file
      create: {        after: async ({ data, ctx }) => {          await auditLog("session.created", {            userId: data.userId,            ip: ctx?.request?.headers.get("x-forwarded-for"),            userAgent: ctx?.request?.headers.get("user-agent"),          });        },
SKILL.md:287In the instructionsOpen original file
      update: {        after: async ({ data, oldData }) => {          if (oldData?.email !== data.email) {            await auditLog("user.email_changed", {              userId: data.id,              oldEmail: oldData?.email,              newEmail: data.email,            });          }
Start here · InstructionsSKILL.md
better-auth-security-best-practices
Lines connect the instruction file to its sections, not an observed execution order. Select a section to read the source. 5 more sections are available in the original file.
Files and check records1 files

Coverage and gaps

Content covered in each file

These are the source ranges included in this check, not a guarantee that every issue has been resolved.

  • SKILL.mdFull text included

This report is for the version above. We read the available code and instructions without running the skill or checking extra packages it installs. This is not a promise of safety: a different version or setup may behave differently.

  • SKILL.mdInstructions

Operations mentioned in code and instructions

Read keys or account settings
SKILL.md:14In the instructionsOpen original file
export const auth = betterAuth({  secret: process.env.BETTER_AUTH_SECRET, // or via `BETTER_AUTH_SECRET` env});
SKILL.md:337In the instructionsOpen original file
Built-in: consistent response messages, dummy operations on invalid requests, background email sending. Return generic error messages ("Invalid credentials") rather than specific ones ("User not found").
SKILL.md:345In the instructionsOpen original file
export const auth = betterAuth({  secret: process.env.BETTER_AUTH_SECRET,  baseURL: "https://api.example.com",
Connect to websites
SKILL.md:117In the instructionsOpen original file
export const auth = betterAuth({  baseURL: "https://api.example.com",  trustedOrigins: [
SKILL.md:119In the instructionsOpen original file
  trustedOrigins: [    "https://app.example.com",    "https://admin.example.com",
SKILL.md:120In the instructionsOpen original file
    "https://app.example.com",    "https://admin.example.com",  ],
Lines read
433
File checksum (to compare versions)
5f6ddb851ceee643414802b185180fbdf33a7d2440403c7a3fdbe5e4a7c5c0b4