Skip to main content
interlace
Plugin: browser-securityRules

no-credentials-in-query-params

CWE-598

Quick Summary

AspectDetails
SeverityHigh (Credential Exposure)
Auto-Fix❌ No (requires moving data to Body)
CategorySecurity
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForWeb and Mobile applications using APIs

Vulnerability and Risk

Vulnerability: Passing sensitive information (like passwords, API keys, or session tokens) within URL query parameters (GET requests).

Risk: URL query parameters are frequently stored in browser history, server logs, and web analytics platforms. They are also visible in the Referer header sent to third-party sites. An attacker with access to these logs or history can easily recover user credentials.

Error Message Format

The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:

🔒 CWE-598 OWASP:M1 | Credentials in Query Params detected | HIGH [CredExposure]
   Fix: Do not pass sensitive data in URL; use POST request body or headers | https://cwe.mitre.org/data/definitions/598.html

Message Components

ComponentPurposeExample
Risk StandardsSecurity benchmarksCWE-598 OWASP:M1
Issue DescriptionSpecific vulnerabilityCredentials in Query Params detected
Severity & ComplianceImpact assessmentHIGH [CredExposure]
Fix InstructionActionable remediationUse POST request body or headers
Technical TruthOfficial referenceSensitive Query Strings

Rule Details

This rule flags strings and template literals whose query string (?name= / &name=) carries a credential parameter. A parameter name is a credential when it ends in a credential word, compared as whole words after splitting on _, -, . and case boundaries:

  • password, passwd, pwd, passphrase, secret, apikey / api key — so client_secret, api_key, apiKey, new_password all count;
  • access token, refresh token, id token, auth token, session token, bearer token, private token — access_token is the RFC 6750 §2.3 URI-query bearer token;
  • token and auth only as the whole name.

A name that merely starts with a credential word (secret_name, password_policy) or ends in an unrelated token (pageToken, stateToken) is not a credential. A token on an out-of-band verification link (/reset-password?token=, /verify-email?token=) is exempt — that flow has no other channel. A string in a request-body position (body, data, form, formData, or new URLSearchParams(…)) is a form-encoded body, where credentials belong, and is not reported.

Partition: this rule owns every credential in the query string, passwords included. A password in the URL's userinfo (https://user:pass@host) or fragment (#password=) is no-password-in-url's finding.

Why This Matters

IssueImpactSolution
🕵️ LeakageCredentials stored in logs/historySend sensitive data in request body only
🚀 ExfiltrationReferer header leaks tokensUse Authorization: Bearer <token> header
🔒 ComplianceFailure to meet security benchmarksImplement strict transport security and data encapsulation

Configuration

The rule has two options (see Options): bodyProperties, the property names under which a string is a form-encoded request body, and credentialSuffixes, the credential words a parameter name ends in. Extend bodyProperties for a client that names its payload something else:

{
  "rules": {
    "browser-security/no-credentials-in-query-params": [
      "error",
      { "bodyProperties": ["body", "data", "form", "formData", "payload"] }
    ]
  }
}

Examples

❌ Incorrect

// Hardcoded credentials in a query string
const url = '/api/login?username=user&password=pass123';

// Passing a token in the URL for a search query
fetch('/api/search?token=abc123&query=test');

// Prefixed spellings of the same credentials
fetch('/api/data?access_token=abc123&lang=en');
fetch(`/oauth/token?client_secret=${secret}`);
const maps = '/maps?lang=en&api_key=abc123';

✅ Correct

// Safe URL without credentials
const url = '/api/users?page=1&limit=10';

// A pagination cursor is not a credential
fetch('/api/items?pageToken=abc&limit=10');

// Passing sensitive data in the request body (Secure)
fetch('/api/login', {
  method: 'POST',
  body: JSON.stringify({ username: 'user', password: 'password' }),
});

// Passing credentials in an Authorization header (Secure)
fetch('/api/data', {
  headers: {
    Authorization: 'Bearer abc123',
  },
});

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Values from Variables

Why: This rule performs simple string matching on literals. It does not trace values stored in variables.

// ❌ NOT DETECTED
const pName = 'password';
const pVal = 'secret';
const url = `/api?${pName}=${pVal}`;

Mitigation: Always use a URL builder library or a secure request utility.

Dynamic URL Construction

Why: If URLs are built using non-literal methods (e.g., URLSearchParams object), they might be missed.

// ❌ NOT DETECTED
const params = new URLSearchParams();
params.append('password', '123');
const url = '/api?' + params.toString();

Mitigation: Standardize on a secure API caller that automatically handles sensitive data.

Options

OptionTypeDefaultDescription
bodyPropertiesstring[]["body","data","form","formData"]Property names whose value is a request body rather than a URL
credentialSuffixesstring[]see belowCredential words a query-parameter name ends in (replaces the default)

credentialSuffixes

A parameter is a credential when its name ENDS in one of these words, matched as whole words: ?access_token=, ?client_secret= and ?apiKey= all report, while ?secret_name= (names a secret, does not carry one) does not. A bare ?token= or ?auth= reports regardless of this option.

The default is password, passwd, pwd, passphrase, secret, apikey, api key, access token, refresh token, id token, auth token, session token, bearer token and private token. Any spelling of an entry works — apiKey, api_key and api key are the same words.

Setting the option replaces the list. A domain where ?pwd= is a working directory, not a password, drops that entry:

{
  "rules": {
    "browser-security/no-credentials-in-query-params": [
      "error",
      {
        "credentialSuffixes": [
          "password",
          "passphrase",
          "secret",
          "apikey",
          "access token"
        ]
      }
    ]
  }
}

bodyProperties

A string in one of these positions is form-encoded payload, not a query string, and credentials belong there. RFC 6749 §2.3.1 prescribes exactly this for OAuth 2.0 token introspection and the client-credentials grant:

fetch(introspectUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: `client_id=${id}&client_secret=${secret}&token=${token}`,
});

The defaults cover fetch/undici (body), axios (data), got and request (form, formData); extend the list for a client that names its payload something else.

The same string in a URL still reports — only the position is exempt.

References

Use this rule

npm install -D oxlint-plugin-browser-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-browser-security"],  "rules": {    "browser-security/no-credentials-in-query-params": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-browser-security'],    rules: {      'browser-security/no-credentials-in-query-params': 'error',    },  },});

oxlint-plugin-browser-security re-exports eslint-plugin-browser-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.