Api Key Exposure in Sails with Basic Auth
Api Key Exposure in Sails with Basic Auth — how this specific combination creates or exposes the vulnerability
Sails.js is a Node.js web framework that encourages rapid API development. When Basic Authentication is used naively, it can inadvertently expose or amplify the risk of API key exposure. In Sails, Basic Auth is commonly implemented via policies that decode the Authorization header, base64-decode the credentials, and validate them against a user store or an external service.
The vulnerability arises when developers conflate static API keys with Basic Auth credentials, or when Basic Auth credentials are treated as bearer-style secrets. For example, if a client sends an API key in a custom header (e.g., x-api-key) and that value is also embedded in the Basic Auth credentials (username or password), the key can be extracted trivially by an authenticated attacker. Additionally, if Sails logs request headers or errors in verbose mode, Basic Auth credentials and any associated keys can appear in logs, creating a data exposure risk.
Consider a Sails policy that extracts credentials like this:
module.exports.extractCredentials = function (req) {
const authHeader = req.headers.authorization || '';
const base64 = authHeader.replace(/^Basic\s+/, '');
const decoded = Buffer.from(base64, 'base64').toString('utf8');
const [username, password] = decoded.split(':');
return { username, password };
};
If password is used as an API key or contains a key, any middleware or logging that inspects the request can inadvertently expose it. Because Basic Auth transmits credentials on every request (base64-encoded, not encrypted), interception via insecure transport or log leakage leads directly to API key exposure. This is especially risky when TLS is misconfigured or when internal proxies or load balancers terminate TLS improperly, allowing credentials and keys to be observed in transit or at rest in logs.
In an unauthenticated scan, middleBrick tests for data exposure by inspecting response headers and payloads for sensitive patterns. When Basic Auth is used in Sails, the scanner can detect whether credentials or derived keys appear in responses, error messages, or insecurely logged requests. Findings typically highlight the risk of credential theft and recommend removing secrets from logs and ensuring transport encryption is properly enforced.
To mitigate this specific combination, Sails applications should avoid placing API keys inside Basic Auth credentials and should enforce strict transport security. Keys must be stored server-side, never echoed in responses or logs, and rotated independently of authentication credentials. middleBrick’s cross-referencing of OpenAPI specs with runtime behavior helps identify mismatches where specs declare security schemes but runtime behavior exposes sensitive values.
Basic Auth-Specific Remediation in Sails — concrete code fixes
Remediation focuses on preventing the leakage of credentials and any associated keys, enforcing encryption, and avoiding embedding secrets in places where they can be logged or exposed. Below are concrete, safe patterns for Sails.
1. Use a dedicated authentication policy without logging credentials
Ensure your policy does not log or echo credentials. Do not include the password or derived key in any response or server-side log.
// api/policies/basic-auth-safe.js
module.exports.basicAuthSafe = async function (req, res, proceed) {
const authHeader = req.headers.authorization || '';
if (!authHeader.startsWith('Basic ')) {
return res.unauthorized('Missing Basic Auth');
}
const base64 = authHeader.replace(/^Basic\s+/, '');
let decoded;
try {
decoded = Buffer.from(base64, 'base64').toString('utf8');
} catch (e) {
return res.badRequest('Invalid Basic Auth encoding');
}
const [username, password] = decoded.split(':');
if (!username || !password) {
return res.unauthorized('Invalid credentials');
}
// Validate credentials securely (e.g., compare hashed password)
const isValid = await validateUserCredentials(username, password);
if (!isValid) {
return res.unauthorized('Invalid credentials');
}
// Do NOT log password or any key derived from it
req.sails.log.verbose(`Auth attempt for user: ${username}`);
return proceed();
};
async function validateUserCredentials(username, password) {
// Use a constant-time comparison for hashed passwords
const user = await User.findOne({ username });
if (!user) return false;
return await sails.helpers.passwords.compare(password, user.passwordHash);
}
2. Do not embed API keys in Basic Auth credentials
Keep API keys out of the username/password string. Store keys in environment variables or a secrets manager, and reference them in Sails config without exposing them in requests.
// config/custom.js
module.exports.custom = {
apiKeys: {
paymentService: process.env.PAYMENT_API_KEY,
analytics: process.env.ANALYTICS_API_KEY,
},
basicAuth: {
username: process.env.BASIC_AUTH_USER,
passwordHash: process.env.BASIC_AUTH_PASSWORD_HASH, // store a hash, not plaintext
},
};
// api/helpers/payment-request.js
module.exports.callPayment = async function (order) {
const apiKey = sails.config.apiKeys.paymentService;
const authHeader = 'Basic ' + Buffer.from(
`${sails.config.basicAuth.username}:PLACEHOLDER`,
'utf8'
).toString('base64');
// Use apiKey as a bearer token in headers, not inside Basic Auth
return sails.helpers.http.get('https://api.payments.example/charge', {
headers: {
Authorization: apiKey.startsWith('Bearer')
? apiKey
: `Bearer ${apiKey}`,
'X-Request-ID': req.id,
},
});
};
3. Enforce HTTPS and disable verbose logging in production
Ensure TLS is correctly configured so credentials cannot be intercepted. Disable verbose request/response logging that might include headers containing credentials.
// config/env/production.js
module.exports.env = function (ctx) {
return {
log: {
level: 'warn', // avoid verbose in production
},
hooks: {
order: ['basic-auth-safe'],
},
// Enforce secure cookies and HSTS if applicable
secureCookies: true,
hsts: {
enabled: true,
maxAge: 31536000,
includeSubDomains: true,
},
};
};
4. Validate and sanitize inputs to prevent injection via auth flows
Even with Basic Auth, validate and sanitize all inputs to prevent SSRF or header injection that could expose credentials via redirects or callback URLs.
// api/policies/validate-headers.js
module.exports.validateHeaders = function (req, res, proceed) {
const authHeader = req.headers.authorization || '';
if (authHeader.includes('\n') || authHeader.includes('\r')) {
return res.badRequest('Invalid characters in headers');
}
return proceed();
};
By separating concerns—using Basic Auth strictly for identity verification and keeping API keys in secure, server-side configuration—you reduce the surface area for exposure. middleBrick’s scans can verify that no credentials appear in responses and that security headers are present, helping you confirm that remediation is effective.