Sensitive information in GET query strings

Sensitive information in GET query strings

Description

Passwords, tokens and API keys placed in URL query strings may remain in browser history, server or proxy logs, shared links and monitoring systems. Depending on the policy and destination, they may also be sent in the Referer header. HTTPS does not remove these storage and sharing paths. An attacker may reuse exposed secrets to take over accounts or sessions, or abuse APIs.

Potential impact

  • Passwords and tokens may be exposed through URL history, logs, monitoring, caches or referrers.
  • Reusing a stolen password or access token may allow account or session takeover.
  • Exposed API keys or access tokens may enable excessive calls, data collection or unexpected charges.

Remediation

  • Keep sensitive information out of URL queries. Use a POST body (req.body) or an authentication header (Authorization) as appropriate for the operation. A POST URL must not contain secrets either.
  • Update the route to read the body or header, and use method="POST" for relevant forms.
  • Use HTTPS and remove or mask secrets in request-body and authentication-header logs.
  • Restrict sensitive query logging in reverse proxies and monitoring systems as well as the application.
  • Reject sensitive GET queries, but also fix the client: a proxy may already have logged the URL before rejection.
  • Do not include secrets in redirect URLs or shared links.

Examples

These excerpts compare where values are sent. Password authentication and token verification are omitted as comments; implement them and return success only after they pass.

Before

javascript
const express = require("express");
const app = express();

// BAD: Read a password from the GET query
app.get("/signin", (req, res) => {
  const user = req.query.user; // Non-secret example value
  const password = req.query.password; // Exposed in the URL
  // authenticate(user, password)
  res.send("signed in");
});

app.listen(3000);

After

javascript
const express = require("express");
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// GOOD: Read from the POST body
app.post("/signin", (req, res) => {
  const { user, password } = req.body; // Send the secret in the body
  // authenticate(user, password)
  res.send("signed in");
});

// Alternatively, send tokens in the Authorization header
app.post("/api/data", (req, res) => {
  const auth = req.get("Authorization"); // For example, 'Bearer <token>'
  // verifyBearer(auth)
  res.json({ ok: true });
});

app.listen(3000);

Explanation:

  • Before: The password is part of the URL, which may remain in history or logs and, depending on policy, be sent as a referrer.
  • After: The POST body or Authorization header keeps the secret out of the URL. HTTPS and log protection are still required.

References