Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const CLIOptionsSchema = z.object({
help: z.boolean().default(false),
/** Max concurrent trigger/skill executions (default: 4) */
parallel: z.number().int().positive().optional(),
/** Model to use for analysis (fallback when not set in config) */
model: z.string().optional(),
// Verbosity options
quiet: z.boolean().default(false),
verbose: z.number().default(0),
Expand Down Expand Up @@ -68,6 +70,7 @@ Targets:
Options:
--skill <name> Run only this skill (default: run all built-in skills)
--config <path> Path to warden.toml (default: ./warden.toml)
-m, --model <model> Model to use (fallback when not set in config)
--json Output results as JSON
--fail-on <severity> Exit with code 1 if findings >= severity
(critical, high, medium, low, info)
Expand Down Expand Up @@ -203,6 +206,7 @@ export function parseCliArgs(argv: string[] = process.argv.slice(2)): ParsedArgs
options: {
skill: { type: 'string' },
config: { type: 'string' },
model: { type: 'string', short: 'm' },
json: { type: 'boolean', default: false },
'fail-on': { type: 'string' },
'comment-on': { type: 'string' },
Expand Down Expand Up @@ -312,6 +316,7 @@ export function parseCliArgs(argv: string[] = process.argv.slice(2)): ParsedArgs
targets: targets.length > 0 ? targets : undefined,
skill: values.skill,
config: values.config,
model: values.model,
json: values.json,
failOn: values['fail-on'] as Severity | undefined,
commentOn: values['comment-on'] as Severity | undefined,
Expand Down
10 changes: 7 additions & 3 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,10 @@
reporter.blank();
}

// Try to load config for custom skills
// Try to load config for custom skills and defaults
let repoPath: string | undefined;
let skillsConfig: SkillDefinition[] | undefined;
let defaultsModel: string | undefined;

try {
repoPath = getRepoRoot(cwd);
Expand All @@ -110,13 +111,16 @@
if (existsSync(configPath)) {
const config = loadWardenConfig(dirname(configPath));
skillsConfig = config.skills;
defaultsModel = config.defaults?.model;
}
} catch {
// Not in a git repo or no config - that's fine
}

// Build skill tasks
const runnerOptions: SkillRunnerOptions = { apiKey, abortController };
// Model precedence: defaults.model > CLI flag > WARDEN_MODEL env var > SDK default
const model = defaultsModel ?? options.model ?? process.env['WARDEN_MODEL'];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Model precedence order is inverted from stated intent

High Severity

The model precedence implementation contradicts the PR description. The PR states "CLI flag > trigger config > defaults config > env var" but the code implements the opposite: defaultsModel ?? options.model ?? process.env['WARDEN_MODEL'] gives config higher priority than the CLI flag. When a user passes --model claude-opus-4-5-20250514, they expect it to override config settings, but currently config values win.

Additional Locations (1)

Fix in Cursor Fix in Web

const runnerOptions: SkillRunnerOptions = { apiKey, model, abortController };

Check warning on line 123 in src/cli/main.ts

View check run for this annotation

@sentry/warden / warden: security-review

Environment Variable Injection in Model Selection

The code reads from process.env['WARDEN_MODEL'] without validation and passes it directly to the model configuration. If an attacker can control environment variables (e.g., in a CI/CD pipeline or shared hosting environment), they could potentially inject malicious model names or attempt to manipulate the behavior of the application. While the actual exploitability depends on how the 'model' parameter is validated downstream, it's a best practice to validate or sanitize environment variable inputs, especially when they control application behavior.
Comment on lines +122 to +123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Environment Variable Injection in Model Selection

The code reads from process.env['WARDEN_MODEL'] without validation and passes it directly to the model configuration. If an attacker can control environment variables (e.g., in a CI/CD pipeline or shared hosting environment), they could potentially inject malicious model names or attempt to manipulate the behavior of the application. While the actual exploitability depends on how the 'model' parameter is validated downstream, it's a best practice to validate or sanitize environment variable inputs, especially when they control application behavior.

Suggested fix: Add validation to ensure the model name matches expected patterns or is from an allowlist of valid model names before using it.

Suggested change
const model = defaultsModel ?? options.model ?? process.env['WARDEN_MODEL'];
const runnerOptions: SkillRunnerOptions = { apiKey, model, abortController };
const envModel = process.env['WARDEN_MODEL'];
// Validate model name if from environment
if (envModel && !/^[a-z0-9-]+$/.test(envModel)) {
reporter.error('Invalid model name in WARDEN_MODEL environment variable');
return 1;
}
const model = defaultsModel ?? options.model ?? envModel;

warden: security-review

const tasks: SkillTaskOptions[] = skillNames.map((skillName) => ({
name: skillName,
failOn: options.failOn,
Expand Down Expand Up @@ -374,7 +378,7 @@
reporter.success(`Loaded ${config.triggers.length} ${pluralize(config.triggers.length, 'trigger')}`);

// Resolve triggers with defaults and match
const resolvedTriggers = config.triggers.map((t) => resolveTrigger(t, config));
const resolvedTriggers = config.triggers.map((t) => resolveTrigger(t, config, options.model));
const matchedTriggers = resolvedTriggers.filter((t) => matchTrigger(t, context));

// Filter by skill if specified
Expand Down
50 changes: 50 additions & 0 deletions src/config/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,54 @@ describe('resolveTrigger', () => {
expect(resolved.actions).toEqual(['opened']);
expect(resolved.skill).toBe('security-review');
});

describe('model precedence', () => {
it('trigger.model takes precedence over cliModel', () => {
const trigger: Trigger = {
...baseTrigger,
model: 'claude-opus-4-20250514',
};

const resolved = resolveTrigger(trigger, baseConfig, 'claude-haiku-3-5-20241022');

expect(resolved.model).toBe('claude-opus-4-20250514');
});

it('defaults.model takes precedence over cliModel', () => {
const config: WardenConfig = {
...baseConfig,
defaults: {
model: 'claude-sonnet-4-20250514',
},
};

const resolved = resolveTrigger(baseTrigger, config, 'claude-haiku-3-5-20241022');

expect(resolved.model).toBe('claude-sonnet-4-20250514');
});

it('cliModel is used when no config model is set', () => {
const resolved = resolveTrigger(baseTrigger, baseConfig, 'claude-haiku-3-5-20241022');

expect(resolved.model).toBe('claude-haiku-3-5-20241022');
});

it('trigger.model takes precedence over defaults.model', () => {
const trigger: Trigger = {
...baseTrigger,
model: 'claude-opus-4-20250514',
};
const config: WardenConfig = {
...baseConfig,
triggers: [trigger],
defaults: {
model: 'claude-sonnet-4-20250514',
},
};

const resolved = resolveTrigger(trigger, config, 'claude-haiku-3-5-20241022');

expect(resolved.model).toBe('claude-opus-4-20250514');
});
});
});
16 changes: 14 additions & 2 deletions src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,21 @@
/**
* Resolve a trigger's configuration by merging with defaults.
* Trigger-specific values override defaults.
*
* Model precedence (highest to lowest):
* 1. trigger.model (warden.toml trigger-level)
* 2. defaults.model (warden.toml [defaults])
* 3. cliModel (--model flag)
* 4. WARDEN_MODEL env var
* 5. SDK default (not set here)
*/
export function resolveTrigger(trigger: Trigger, config: WardenConfig): ResolvedTrigger {
export function resolveTrigger(
trigger: Trigger,
config: WardenConfig,
cliModel?: string
): ResolvedTrigger {
const defaults = config.defaults;

Check warning on line 73 in src/config/loader.ts

View check run for this annotation

@sentry/warden / warden: security-review

Environment variable used directly without validation

The code reads process.env['WARDEN_MODEL'] and uses it directly in the model precedence chain without validation. If an attacker can control environment variables (e.g., in CI/CD pipelines, containerized environments, or through process spawning), they could potentially inject malicious model names or values that might be interpreted by downstream code in unintended ways. While the impact depends on how the model value is used later, accepting arbitrary strings from environment variables without validation is a security risk.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Environment variable used directly without validation

The code reads process.env['WARDEN_MODEL'] and uses it directly in the model precedence chain without validation. If an attacker can control environment variables (e.g., in CI/CD pipelines, containerized environments, or through process spawning), they could potentially inject malicious model names or values that might be interpreted by downstream code in unintended ways. While the impact depends on how the model value is used later, accepting arbitrary strings from environment variables without validation is a security risk.

Suggested fix: Add validation to ensure the environment variable value matches expected patterns (e.g., allowed model names, format validation). Consider using a whitelist of allowed model names or at minimum, validate that it matches expected patterns.

Suggested change
const defaults = config.defaults;
const envModelRaw = process.env['WARDEN_MODEL'];
// Validate env var: alphanumeric, hyphens, dots, max 100 chars
const envModel = envModelRaw && /^[a-zA-Z0-9._-]{1,100}$/.test(envModelRaw)
? envModelRaw : undefined;

warden: security-review

const envModel = process.env['WARDEN_MODEL'];

return {
...trigger,
Expand All @@ -73,6 +85,6 @@
maxFindings: trigger.output?.maxFindings ?? defaults?.output?.maxFindings,
labels: trigger.output?.labels ?? defaults?.output?.labels,
},
model: trigger.model ?? defaults?.model,
model: trigger.model ?? defaults?.model ?? cliModel ?? envModel,

Check warning on line 88 in src/config/loader.ts

View check run for this annotation

@sentry/warden / warden: security-review

Unvalidated environment variable used in model configuration

The WARDEN_MODEL environment variable is read from process.env and used directly without validation or sanitization. If this model value is later passed to an external API, command execution, or used in security-sensitive contexts, an attacker with control over environment variables could potentially inject malicious values. The impact depends on how the model value is used downstream - if it's passed to shell commands, file paths, or external services without validation, this could lead to injection vulnerabilities.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Unvalidated environment variable used in model configuration

The WARDEN_MODEL environment variable is read from process.env and used directly without validation or sanitization. If this model value is later passed to an external API, command execution, or used in security-sensitive contexts, an attacker with control over environment variables could potentially inject malicious values. The impact depends on how the model value is used downstream - if it's passed to shell commands, file paths, or external services without validation, this could lead to injection vulnerabilities.

Suggested fix: Validate the environment variable against an allowlist of acceptable model values before using it. If the model names are known, use a whitelist approach. Otherwise, validate the format and reject suspicious characters.

Suggested change
model: trigger.model ?? defaults?.model ?? cliModel ?? envModel,
let envModel = process.env['WARDEN_MODEL'];
// Validate model name to prevent injection
if (envModel && !/^[a-zA-Z0-9._-]+$/.test(envModel)) {
console.warn(`Invalid WARDEN_MODEL format: ${envModel}. Ignoring.`);
envModel = undefined;
}

warden: security-review

};
}