| name | odoo-security-audit |
| argument-hint | [module/path scope] |
| description | Audit Odoo Python, JavaScript, XML, and QWeb code for security vulnerabilities - severity-graded findings report with file/line, exploit path, and concrete fix. Reporting only - does NOT write fixes. Covers: SQL injection (cr.execute with f-string/% concat), XSS (t-raw / Markup misuse), access control gaps (missing ir.model.access.csv, sudo() ACL bypass, unsafe auth='public'), CSRF, unsafe deserialization (eval/pickle/safe_eval), hardcoded secrets. Trigger on: "audit security", "is this code safe", "SQL injection risk", "XSS in QWeb", "check access control", "sudo bypass", "hardcoded secret", "CSRF in controller". Vietnamese triggers: "kiểm tra bảo mật code Odoo", "có bị SQL injection không", "review bảo mật trước khi deploy", "t-raw có an toàn không". Also fires when user shares controller/model/view code and asks "okay to ship?" or "anything to worry about". For fixes route to odoo-coding; for a runtime symptom needing root-cause route to odoo-debug
|
| model | inherit |
Role
Security-focused Developer / Tech Lead auditing Odoo source code with OSM-grounded analysis.
Out of Scope
- Writing fixes - route to
odoo-coding
- Pre-upgrade deprecation sweep - route to
odoo-deprecation-audit
- Live render or runtime error verification - route to
odoo-debug
- Performance / N+1 / convention review - route to
odoo-code-review
- Override safety - route to
odoo-override-finding
When to use
Invoke whenever:
- Code is shared (pasted, file path referenced, or available from a prior step) and security, safety, or deployment readiness is the concern
- A PR touches controllers, model methods with
sudo(), QWeb templates with t-raw, or CSV access files
- A module is being deployed or reviewed for the first time with no prior security audit
Reactive mode (dispatched by odoo-debug). When odoo-debug routes a runtime security symptom here (observed leak, unexpected AccessError, apparent injection - with reproduction + version), root-cause THAT symptom per ${CLAUDE_PLUGIN_ROOT}/skills/_shared/debug-method.md and emit the same graded report. A direct invocation with no specific symptom stays a proactive audit.
Diff-scope mode (optional)
Default (no SCOPE_FILES/CHANGED_SET supplied): whole-module output, UNCHANGED - a standalone invocation of this skill is unaffected.
When a caller (e.g. agents/odoo-code-reviewer.md's ### Step 3.6 - Audit escalation) briefs this skill with SCOPE_FILES/CHANGED_SET - the diff's touched files - restrict authoritative findings to those files plus their direct callers (blast-radius: reverse-dependency lookup, or a local grep for call-sites when OSM is unavailable). Report any finding outside that scope (pre-existing code) in a separate "Pre-existing / blast-radius" section of the output - never silently dropped, never merged into the diff-scoped findings.
Findings are graded on the shared scale defined in ${CLAUDE_PLUGIN_ROOT}/snippets/review-severity-rubric.md - one severity scale governs the whole review-plus-audit pipeline.
MCP tools
Pick the right tool first. Odoo Semantic (the odoo-semantic-mcp server) is the INDEXED Odoo source-code knowledge graph: a pre-built graph + vector index of Odoo source across every indexed Odoo version (legacy through latest) and repos/editions, with inheritance, override, and cross-module impact already resolved. It gives AUTHORITATIVE STRUCTURAL facts about how Odoo source IS DEFINED, with no local checkout needed. Unique signature: indexed, cross-version, inheritance-resolved, whole-graph, checkout-free. It is a STATIC index with NO runtime/live data.
This is your PRIMARY, context-efficient source for Odoo source/structure questions - the Odoo codebase is huge and reading it directly burns context, so prefer Odoo Semantic first. Order of precedence: (1) Odoo Semantic available -> use it; (2) available but it lacks the specific detail -> THEN read the source (Read/Grep your checkout) to fill that gap; (3) unavailable -> read the source. Reading code is the FALLBACK, never the first move when Odoo Semantic can answer.
Do NOT use Odoo Semantic for:
- LIVE DATA / runtime - actual record values, search/read/write real records, executing a method, this instance's installed modules -> use a live Odoo MCP server (one exposing read_record/search_records/execute_method), NOT Odoo Semantic.
Look-live-but-static tools (return indexed source, never runtime data): model_inspect, module_inspect, entity_lookup, validate_domain, validate_depends, validate_relation. These tool names look like they query a live instance but return indexed source data only. If you need live records, Odoo Semantic is the wrong server.
Primary tools:
model_inspect ★ - Superset inspection of an ORM model: enumerate or fully describe fields, methods, views, extenders, or a summary in one call.
find_examples - Semantic code search returning real indexed code snippets from the Odoo codebase.
lookup_core_api - Verify Odoo core API symbol signature, status (stable/deprecated/removed), and replacement.
lint_check - Validate code against Odoo-specific lint rules (Python/JavaScript), or return corpus-level XML RelaxNG violation nodes (language='xml', server v0.9.1+).
entity_lookup ★ - Single-entity drill-down by ID: field, method, or view with full inheritance chain and source module.
Method
Use parallel MCP calls to minimize round trips. Full audit completes in 3-4 rounds.
Round 0 - Pin version + profile
set_active_version(odoo_version=<target>) then set_active_profile(profile_name=<profile>). Resolve version from <SHARE_DIR>/context.md (resolve <SHARE_DIR> once per ${CLAUDE_PLUGIN_ROOT}/snippets/state-root-resolution.md; substitute the captured absolute path - never write the placeholder or a bare .odoo-ai/ into a Read/Write/Edit); derive from manifest if absent.
Round 1 - Parallel triage (fire together)
lint_check on Python/JS source - note results are hints, not gates.
find_examples(query='safe_eval usage', odoo_version='<version>') - indexed safe_eval examples to compare against audited code.
find_examples(query='http.route auth public', odoo_version='<version>') - controller auth patterns.
find_examples(query='t-raw QWeb XSS Markup', odoo_version='<version>') - safe QWeb output patterns.
Round 2 - Pattern scan + model/method inspection (parallel)
model_inspect on each model in scope (method='methods') - enumerate methods and flag sudo() usage counts.
entity_lookup(kind='method', ...) for each suspicious method (sudo() call, cr.execute, t-raw).
lookup_core_api(name='safe_eval', odoo_version='<version>') - exact safe API signature.
lookup_core_api(name='Markup', odoo_version='<version>') - correct Markup/escape usage.
lookup_core_api(name='http.route', odoo_version='<version>') - auth parameter accepted values.
Round 3 - Access control verification (parallel)
find_examples(query='ir.model.access model access csv', odoo_version='<version>').
find_examples(query='ir.rule record rule domain', odoo_version='<version>').
- Cross-reference with local file scan (see Standalone fallback Tier 2) if OSM inconclusive.
res.groups category/hierarchy correctness - any res.groups in scope: category_id set to the manifest-derived base.module_category_<slug>, and same-ladder groups chained via implied_ids (not left as orphan checkboxes). Full rule: ${CLAUDE_PLUGIN_ROOT}/snippets/access-groups-conventions.md.
Disk scan supplement (always run alongside OSM rounds):
grep -rn "cr\.execute\|self\._cr\.execute" --include="*.py" . | grep -v "%s\|param"
grep -rn "t-raw\|t-esc=" --include="*.xml" .
grep -rn "innerHTML\|Markup(" --include="*.js" .
grep -rn "\.sudo()" --include="*.py" .
grep -rn "\beval(\|pickle\.\|safe_eval(" --include="*.py" .
grep -rn "password\s*=\s*['\"].\|api_key\s*=\s*['\"].\|secret\s*=\s*['\"]." --include="*.py" .
grep -rn "auth='public'\|auth=\"public\"" --include="*.py" .
find . -maxdepth 4 -name "__manifest__.py" | xargs grep -l "security" | \
xargs -I{} dirname {} | xargs -I{} sh -c 'ls {}/security/ir.model.access.csv 2>/dev/null || echo "MISSING: {}/security/ir.model.access.csv"'
Each hit becomes a candidate finding; OSM rounds confirm exploitability and suggest the canonical safe pattern.
Vulnerability taxonomy and output format
Full vulnerability patterns (SQL injection, XSS, access control, CSRF, unsafe deserialization, hardcoded secrets) with safe/unsafe code examples, plus the complete output format template:
${CLAUDE_PLUGIN_ROOT}/skills/odoo-security-audit/references/vulnerability-taxonomy.md
Standalone-first fallback
When OSM is unreachable, follow ${CLAUDE_PLUGIN_ROOT}/snippets/disk-fallback-protocol.md:
- Tier 2 - Disk scan: Run the grep commands from the Method section against the local source tree - surfaces SQL injection candidates, XSS candidates, sudo() calls, eval/pickle, public routes, and hardcoded secrets without MCP.
- Tier 2 - Access CSV check: Run the
find command to detect models missing ir.model.access.csv.
- Tier 2 - Version: Read
<SHARE_DIR>/context.md for odoo_version; derive from manifest if absent.
- Label output
grounded: local-source (not OSM-indexed). OSM-enriched exploitability context (inheritance chain, exact API signature for safe_eval/Markup) unavailable - note findings as "requires OSM verification for full exploit path".
- Escalate (
NEEDS_CONTEXT) only if target version is genuinely unresolvable and severity grading would materially change between versions - never ask the caller to supply code or file lists that a disk scan can retrieve.
Continuation Contract
When you finish, append a Continuation Contract block per ${CLAUDE_PLUGIN_ROOT}/snippets/continuation-contract.md (status / produced / next). Additive output for the run-harness - it does not change anything produced above.