Graphql Introspection in Express with Basic Auth
Graphql Introspection in Express with Basic Auth — how this specific combination creates or exposes the vulnerability
GraphQL introspection in an Express server becomes a security risk when endpoints are unintentionally exposed, especially when protected only by Basic Auth. Introspection queries (query { __schema { queryType { name } } }) reveal the full type model, including queries, mutations, subscriptions, input types, and directives. This metadata can expose sensitive business logic, field names, and relationships that an attacker can use to craft further attacks such as IDOR or property-level authorization abuse.
When Basic Auth is used but not strictly enforced for introspection endpoints, the combination creates a weak boundary: authentication is present, but the schema is still readable without needing valid user credentials for the introspection operation itself. If the Express route handling GraphQL requests does not differentiate between authenticated administrative actions and schema discovery, an unauthenticated or low-privilege actor can retrieve the schema simply by sending an introspection query over HTTP.
In a black-box scan, middleBrick tests unauthenticated attack surfaces and checks whether introspection is allowed by sending an introspection query to the GraphQL endpoint. If the endpoint returns schema details without enforcing stricter authorization, this finding is surfaced with severity and guidance. Attack patterns like this align with OWASP API Top 10 risks such as excessive data exposure and broken object level authorization, because introspection can reveal object identifiers and fields that become targets for BOLA/IDOR.
Real-world examples include endpoints mounted at paths like /graphql or /api/graphql where middleware validates a username and password for mutation requests but does not block introspection queries for any request that passes the basic credentials. Even when Basic Auth is implemented, if the server does not explicitly disable introspection or scope it to privileged roles, the schema remains readable, increasing the attack surface.
middleBrick’s LLM/AI Security checks are relevant here because schema exposure can indirectly aid prompt injection or data exfiltration attempts if the API is coupled with AI-driven tooling. The scanner runs active prompt injection probes and system prompt leakage detection, but it also flags overly open introspection as a discoverability risk that can assist downstream attacks.
To detect this behavior, the scanner compares the OpenAPI/Swagger specification (including full $ref resolution) against runtime responses. If the spec indicates a protected endpoint but runtime introspection is allowed without elevated scopes, a finding is generated with remediation steps tied to the framework and compliance mappings such as OWASP API Top 10 and SOC2 controls.
Basic Auth-Specific Remediation in Express — concrete code fixes
To secure GraphQL introspection in Express with Basic Auth, explicitly disable introspection in production or scope it to authorized requests. Below are concrete, working code examples that demonstrate how to implement these controls.
Example 1: Disable introspection entirely in production
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
const schema = buildSchema(`
type Query {
hello: String
}
`);
const rootValue = {
hello: () => 'world',
};
const app = express();
const isProduction = process.env.NODE_ENV === 'production';
app.use('/graphql', graphqlHTTP((req, res) => ({
schema,
rootValue,
graphiql: !isProduction,
customFormatErrorFn: (error) => ({
message: error.message,
code: error.originalError && error.originalError.code,
}),
validationRules: isProduction ? [] : [], // keep dev tools in dev
// Disable introspection in production by overriding the default behavior
allowedRequestMethods: isProduction ? ['POST'] : ['GET', 'POST'],
})));
app.listen(4000, () => console.log('Server running on port 4000'));
Example 2: Enforce Basic Auth and restrict introspection to admin requests
const auth = require('basic-auth');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
const schema = buildSchema(`
type Query {
public: String
secret: String
}
`);
const rootValue = {
public: () => 'public data',
secret: () => 'secret data',
};
function checkAdmin(credentials) {
return credentials && credentials.name === 'admin' && credentials.pass === 'securepassword';
}
const app = express();
app.use('/graphql', (req, res, next) => {
const user = auth(req);
if (!user) {
res.set('WWW-Authenticate', 'Basic realm="example"');
return res.status(401).send('Authentication required.');
}
if (!checkAdmin(user)) {
return res.status(403).send('Insufficient privileges.');
}
// Attach user context for use in GraphQL resolvers or validation
req.user = user;
next();
});
app.use('/graphql', graphqlHTTP((req, res) => ({
schema,
rootValue,
graphiql: false,
// Optionally disable introspection unless explicitly allowed for admin context
customValidationRules: () => [
// Introspection queries can be blocked by validation rules in custom setups
],
})));
app.listen(4000, () => console.log('Server running on port 4000'));
Example 3: Use a request filter to block introspection queries
const { graphqlHTTP } = require('express-graphql');
const { parse, visit } = require('graphql');
const auth = require('basic-auth');
function isIntrospectionOperation(operation) {
return operation.operation === 'query' &&
operation.selectionSet &&
operation.selectionSet.selections.some(
(sel) => sel.name && sel.name.value === '__schema'
);
}
function stripIntrospection(document) {
return visit(document, {
OperationDefinition(node) {
if (isIntrospectionOperation(node)) {
throw new Error('Introspection queries are not allowed');
}
},
});
}
const app = express();
app.use('/graphql', (req, res, next) => {
const user = auth(req);
if (!user || user.name !== 'admin') {
res.set('WWW-Authenticate', 'Basic');
return res.status(401).send('Authentication required.');
}
req.rawBody = '';
req.setEncoding('utf8');
req.on('data', (chunk) => { req.rawBody += chunk; });
req.on('end', () => {
try {
const document = parse(req.rawBody);
stripIntrospection(document);
next();
} catch (e) {
res.status(400).send(e.message);
}
});
});
app.use('/graphql', graphqlHTTP((req, res) => ({
schema,
rootValue,
graphiql: false,
})));
app.listen(4000, () => console.log('Server running on port 4000'));
Operational guidance
- Prefer POST-only methods for production introspection settings to reduce accidental exposure via browser preflight or logs.
- Combine Basic Auth with HTTPS to protect credentials in transit; Basic Auth sends credentials in an easily decoded header.
- Use environment-based configuration to toggle introspection and graphiql, ensuring development features are not present in production.
- Consider additional authorization checks inside resolvers for field-level security, even when introspection is disabled.
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |