AI Workflows: Vulnerabilities

finding security and correctness issues before they ship

1. Summary

Claude serves well as a first-pass security and correctness reviewer - not a replacement for a formal audit, but effective at catching common issue classes before they reach production. The most effective approach uses role framing and a specific threat category per session. Asking Claude to "review for security" produces a generic scan; asking it to act as an auditor focused on path handling produces findings you can act on. This workflow does not require an md file for one-off reviews. For recurring reviews across a project, a vuln.md file can encode the threat model, scope, and severity classification conventions your team uses.

2. Threat Categories Worth Reviewing

CategoryWhat to check
Path traversal Paths constructed from user input - can they escape the intended root?
Command injection User input reaching shell commands or subprocess calls without sanitization
Input validation User-supplied strings, numbers, or paths accepted without range or format checks
Panics on valid input unwrap(), expect(), array indexing, integer overflow on inputs you don't fully control
Resource leaks File handles, network connections, or allocations that may not be released on all paths
Race conditions Shared mutable state accessed across threads without synchronization
Symlink following Directory traversal that follows symlinks out of the intended tree
Information leakage Error messages or logs that expose internal paths, credentials, or stack traces

3. Example Prompts

Path handling review:
Review [file.rs] as a security auditor focused on path handling.
- Can any constructed path escape the intended root directory?
- Are symlinks followed? Should they be?
- Is user input sanitized before it reaches path construction?
Cite file:line for each finding. Rate each as: safe, needs review, or unsafe.
Command injection review:
Review [script.py] for command injection.
Identify every point where user-supplied input reaches a shell command,
subprocess call, or eval. For each, state whether it is safe or unsafe and why.
Cite file:line for each finding.
Panic surface review (Rust):
Review [file.rs] for panics on valid input.
Find every unwrap(), expect(), and direct index operation on a slice or vec.
For each, state whether the panic is reachable from external input.
Propose a safe alternative only where the panic is reachable.
Input validation sweep:
Review [module] for missing input validation.
Identify every location where data from outside the program boundary
(CLI args, environment variables, files, network) is used without
checking its range, format, or presence. Cite file:line for each gap.

4. Systematic Review Across a Codebase

When reviewing more than two or three files, a structured approach prevents gaps in coverage. Review one file at a time, confirm each finding, and ask before moving to the next:
I want to audit [module] for [threat category].
Files to review: [list]

Start with [file1]. When done with that file, list each finding with
file:line and severity. Then wait for me to approve before moving to [file2].
This pacing matters. A single prompt asking Claude to review ten files produces a list of findings too long to verify. File-by-file review produces findings you can address before the next file is touched.

5. Acting on Findings

After a review session, don't fix findings in the same session. Findings from analysis belong in a new session where the fix is the explicit goal - and where refactor.md or a targeted prompt keeps the fix from expanding scope.
From the security review, the finding at [file:line] is: [paste finding].

Fix only that issue. Do not change anything else in the file.
Show the diff only.
Handing the finding explicitly prevents Claude from re-deriving it (and possibly finding a different issue) and keeps the fix focused on what you verified.

6. Case Study: CppNoSqlDB

This review was run against three headers in CppNoSqlDB before any changes were planned. The vulner.md file encodes the files, the threat categories, and the required table layout so the output format is consistent across sessions and reviewers.

vulner.md

# Vulnerability Review Context

## Target
CppNoSqlDB -- C++ NoSql key-value database:
  DbCore/DbCore.h    core database and element types
  Query/Query.h      query conditions and execution
  Persist/Persist.h  XML persistence and sharding

## Mode
Read-only analysis. Do not change any files.
For each finding: name the vulnerability, cite file:line, describe the problem,
and propose a concrete fix.

## Categories to Check
- Null pointer dereferences (uninitialized pointers, missing null guards)
- Uninitialized members (undefined behavior on first use)
- Out-of-bounds access (unchecked vector indexing, unchecked XML structure)
- Input validation gaps (unvalidated regex patterns passed to std::regex)
- Silent failure (functions that return success regardless of outcome)
- Exception safety (non-portable exception construction, resource leaks on throw)

## Output Format
Report as a table with these columns:
  Vulnerability | File:Line | Description | Fix
List entries in severity order: critical first, then high, then medium.

Prompt

Read vulner.md.
Read DbCore/DbCore.h.
Read Query/Query.h.
Read Persist/Persist.h.

Find every vulnerability in the categories listed in vulner.md.
Report as the table specified in vulner.md.
Do not change any files.

Findings

Vulnerability Location Description Fix
Null pointer dereference in Conditions Query.h:84, 118, 128, 138, 150 pDbElem_ is initialized to nullptr (line 84). Every match*() method dereferences it unconditionally (lines 118, 128, 138, 150). Calling match() before value() is undefined behavior with no diagnostic. Change match() to accept const DbElement<P>& directly and remove the pointer. Or add if (pDbElem_ == nullptr) return false; at the top of each match*() method.
Uninitialized pDb_ in default constructor Query.h:167 Query() {} leaves pDb_ uninitialized. Any subsequent call to select(), from(), or keys() dereferences an indeterminate pointer. The two-argument constructor initializes it correctly; the default does not. Delete the default constructor (Query() = delete;) or initialize pDb_ to nullptr and guard every use with a null check.
Unvalidated regex causes silent failure Query.h:60, 117 name(RegExp re) and description(RegExp re) store patterns without validation (line 60). std::regex re(nameRegExp_) at line 117 throws std::regex_error on a malformed pattern; match() catches all exceptions silently and returns false (lines 104-108). Every record silently fails to match with no error reported. Validate the pattern at the setter: construct std::regex in name() and description() and propagate std::invalid_argument if it throws. Don't defer validation to match time.
Unchecked vector index in XML parsing Persist.h:196, 226 pChild->children()[0]->value() (line 196) and pChild->children()[0]->value() (line 226) access index 0 without checking vector size. Malformed or truncated XML with an empty element produces out-of-bounds access. Guard both sites: if (!pChild->children().empty()) before each [0] access. Use .at(0) to get a bounds-checked throw instead of undefined behavior if a guard is impractical.
fromXml always reports success Persist.h:179, 240 fromXml() unconditionally returns true (line 240) regardless of whether parsing succeeded. A caller that checks the return value to confirm the database was loaded correctly will always see success, even when the XML was empty or malformed and no records were loaded. Track a success flag during parsing. Return false if no records were found when records were expected, or if any required field (key, value) was missing in a record.
Non-portable exception construction DbCore.h:252, 267 throw(std::exception("key does not exist in db")) passes a string to std::exception's constructor. The standard std::exception does not accept a string argument; this relies on an MSVC extension. Code fails to compile on GCC or Clang. Replace with throw std::runtime_error("key does not exist in db"); which is portable and carries the message through what().