Spring `application.{properties,yml}` keys (and Shopify Liquid `{% schema %}`
blocks) were storing the config VALUE in the node docstring, and
`codegraph_explore`'s source section re-read the raw `key = value` line off
disk — so a secret committed to a config file (DB password, API key, JDBC URL
with embedded credentials) could be pushed into an agent's context via
explore/node output without the agent ever opening the file.
Config-leaf nodes (`kind: 'constant'` in a config language) now surface the KEY
only, via a shared `isConfigLeafNode` predicate applied at both surfacing
paths: the value is dropped from extraction, `getCode`/`includeCode` returns
the key instead of the file line, and explore excludes config leaves from
source rendering. The predicate can't match real code (real constants are
ts/java/go/…), so `@Value`/`@ConfigurationProperties` resolution and impact are
unaffected. Adds a regression test asserting a planted secret never appears in
`codegraph_explore` / `codegraph_node` output while the keys still resolve.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
80db274e5f
commit
112e278b5c
@@ -24,7 +24,7 @@ import { QueryBuilder } from '../db/queries';
|
||||
import { GraphTraverser } from '../graph';
|
||||
import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
|
||||
import { logDebug } from '../errors';
|
||||
import { validatePathWithinRoot } from '../utils';
|
||||
import { validatePathWithinRoot, isConfigLeafNode } from '../utils';
|
||||
import { isTestFile, extractSearchTerms, scorePathRelevance, getStemVariants, isDistinctiveIdentifier } from '../search/query-utils';
|
||||
import { LOW_CONFIDENCE_MARKER } from './markers';
|
||||
|
||||
@@ -1161,6 +1161,14 @@ export class ContextBuilder {
|
||||
* Extract code from a node's source file
|
||||
*/
|
||||
private async extractNodeCode(node: Node): Promise<string | null> {
|
||||
// SECURITY (#383): a config-leaf node's on-disk line is `key = <secret>`.
|
||||
// Return the KEY only — never read the value off disk. This closes the
|
||||
// includeCode / buildContext code-block path, mirroring the explore source
|
||||
// renderer; an agent that genuinely needs a value can read the file itself.
|
||||
if (isConfigLeafNode(node)) {
|
||||
return node.signature || node.qualifiedName || node.name;
|
||||
}
|
||||
|
||||
const filePath = validatePathWithinRoot(this.projectRoot, node.filePath);
|
||||
|
||||
if (!filePath || !fs.existsSync(filePath)) {
|
||||
|
||||
@@ -313,7 +313,10 @@ export class LiquidExtractor {
|
||||
endLine,
|
||||
startColumn: match.index - this.getLineStart(startLine),
|
||||
endColumn: 0,
|
||||
docstring: schemaContent?.trim().substring(0, 200), // Store first 200 chars as docstring
|
||||
// SECURITY (#383): don't dump the raw {% schema %} JSON (section settings
|
||||
// + default values) into the docstring — the schema name is already in
|
||||
// `name`, so the data block adds nothing but a potential leak of any
|
||||
// IDs/endpoints/keys a developer placed in setting defaults.
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
|
||||
+6
-1
@@ -26,7 +26,7 @@ import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
} from 'fs';
|
||||
import { clamp, validatePathWithinRoot, validateProjectPath } from '../utils';
|
||||
import { clamp, validatePathWithinRoot, validateProjectPath, isConfigLeafNode } from '../utils';
|
||||
import { isGeneratedFile } from '../extraction/generated-detection';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
|
||||
@@ -1705,6 +1705,11 @@ export class ToolHandler {
|
||||
for (const node of subgraph.nodes.values()) {
|
||||
// Skip import/export nodes — they add noise without information
|
||||
if (node.kind === 'import' || node.kind === 'export') continue;
|
||||
// SECURITY (#383): never render the on-disk source of a config-leaf
|
||||
// (Spring application.{yml,properties} key) — its line is `key = <secret>`,
|
||||
// so whole-file/cluster rendering here would push secrets into context
|
||||
// unbidden. The key still appears in the flow/symbol listing above.
|
||||
if (isConfigLeafNode(node)) continue;
|
||||
|
||||
const group = fileGroups.get(node.filePath) || { nodes: [], score: 0 };
|
||||
group.nodes.push(node);
|
||||
|
||||
@@ -335,7 +335,12 @@ function extractSpringConfig(
|
||||
endColumn: valueText.length,
|
||||
language: lang,
|
||||
signature: dottedKey,
|
||||
docstring: valueText.slice(0, 200),
|
||||
// SECURITY (#383): store the KEY only, never the value. Config files
|
||||
// routinely hold secrets (DB passwords, API keys, JDBC URLs with embedded
|
||||
// credentials), and surfacing the value here pushes it into agent context
|
||||
// unbidden (it lands in codegraph_node/explore output via the docstring).
|
||||
// The key is all `@Value`/`@ConfigurationProperties` resolution needs; an
|
||||
// agent that genuinely needs a value can read the file directly.
|
||||
updatedAt: now,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -46,6 +46,25 @@ const SENSITIVE_PATHS = new Set([
|
||||
'c:\\', 'c:\\windows', 'c:\\windows\\system32',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Config "languages" whose nodes are pure key/value DATA lifted from a config
|
||||
* file (e.g. Spring `application.{yml,properties}`), not source code.
|
||||
*/
|
||||
export const CONFIG_LEAF_LANGUAGES: ReadonlySet<string> = new Set(['yaml', 'properties']);
|
||||
|
||||
/**
|
||||
* A config-leaf node is a single key lifted out of a pure config/data file —
|
||||
* `kind: 'constant'` in a {@link CONFIG_LEAF_LANGUAGES} language. Its on-disk
|
||||
* line is `key = <value>`, and that value is routinely a secret (DB password,
|
||||
* API key, JDBC URL with embedded creds). CodeGraph must surface the KEY only
|
||||
* and never read/return the value, or it pushes secrets into agent context
|
||||
* unbidden — the value isn't needed for resolution, and an agent that genuinely
|
||||
* needs it can read the file directly. (#383)
|
||||
*/
|
||||
export function isConfigLeafNode(node: { kind: string; language?: string }): boolean {
|
||||
return node.kind === 'constant' && !!node.language && CONFIG_LEAF_LANGUAGES.has(node.language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a resolved file path stays within the project root.
|
||||
* Prevents path traversal attacks (e.g. node.filePath = "../../etc/passwd").
|
||||
|
||||
Reference in New Issue
Block a user