API Security Exceptions Management
Issue: #1897
Overview
This document describes the process for managing API security baseline suppressions and exceptions in the Open ACE project.
Security Scanner
The API security scanner (scripts/lint/api_security_scanner.py) detects security violations in Flask routes:
- SEC001: Route handler has no authentication
- SEC002: Route with ID parameter missing ownership check
- SEC003: Blueprint missing
@before_requestauth hook
Baseline File
Security suppressions are stored in scripts/lint/security_baseline.json. Current baseline entries: 0 as of 2026-09-29 (scripts/lint/security_baseline.json). Each suppression must have complete metadata:
{
"key": "SEC002|app/routes/workspace.py|/api/workspace/knowledge/<entry_id>",
"rule": "SEC002",
"file": "app/routes/workspace.py",
"line": 2038,
"endpoint": "/api/workspace/knowledge/<entry_id>",
"message": "Route ... has ID param(s) ['entry_id'] but no ownership check",
"metadata": {
"owner": "@team-backend",
"justification": "Reason why this suppression is necessary",
"reviewed_at": "2026-07-23",
"expires_at": "2027-01-23T00:00:00Z",
"risk_level": "low",
"test_coverage": "tests/integration/routes/test_example.py::test_example",
"alternative_controls": [
"List of alternative security controls in place"
]
}
}
Required Metadata Fields
- owner: GitHub username or team responsible for this suppression
- justification: Clear explanation of why the exception is necessary
- test_coverage: Test file and test name that validates the security control
Optional Metadata Fields
- reviewed_at: Date of last review (ISO format)
- expires_at: Expiration date (ISO format) - CI will fail if expired
- risk_level: Risk assessment (low/medium/high)
- alternative_controls: List of security controls in place
- automated_check: Metadata about automated checks
Adding a New Suppression
Step 1: Document the Exception
Before adding a suppression, ensure:
- You have a valid reason for the exception
- Alternative security controls are in place (e.g., workspace-level access control)
- Test coverage exists or will be added
Step 2: Use @security_annotated Decorator
For endpoints with ownership checks implemented in non-standard patterns:
from app.auth.decorators import security_annotated
@workspace_bp.route("/resource/<int:resource_id>", methods=["GET"])
@security_annotated(reason="Ownership via get_user_resource + permission check")
def get_resource(resource_id):
# Ownership check implemented inline
resource = get_user_resource(user_id, resource_id)
if not resource:
return jsonify({"error": "Access denied"}), 403
...
Step 3: Update Baseline
If the scanner still flags the endpoint after adding @security_annotated:
python scripts/lint/api_security_scanner.py --baseline > scripts/lint/security_baseline.json
Step 4: Add Metadata
Edit scripts/lint/security_baseline.json and add complete metadata for the new suppression.
Step 5: Validate
Run metadata validation:
python scripts/lint/validate_baseline_metadata.py
Removing a Suppression
Suppressions should be removed when:
- Code is fixed and security issue is resolved
- Endpoint is removed
- Alternative security controls are no longer necessary
Step 1: Fix the Issue
Implement proper security controls or remove the endpoint.
Step 2: Regenerate Baseline
python scripts/lint/api_security_scanner.py --baseline > scripts/lint/security_baseline.json
Step 3: Validate
python scripts/lint/api_security_scanner.py
python scripts/lint/validate_baseline_metadata.py
CI Integration
The security scanner runs in CI for every pull request:
- API Security Scanner: Detects new violations not in baseline
- Baseline Diff Check: Detects changes to baseline (requires justification)
- Metadata Validation: Ensures all suppressions have complete metadata
Exception Lifecycle
Every suppression follows the same lifecycle: add with metadata → quarterly review → renew or remove. This section is the authoritative process for security-baseline exceptions — including the skip-security-check PR bypass label — for all baseline guides; BANDIT_BASELINE.md links here instead of restating it.
Quarterly Audit
Baseline suppressions are audited quarterly (January/April/July/October):
- Check
expires_atdates - CI will fail if expired - Review
ownerandjustification- ensure they're still accurate - Verify
test_coverage- ensure tests still pass - Assess
alternative_controls- ensure they're still effective
Audit reminders are currently manual — no scheduled workflow audits these suppressions yet (the quarterly false-positive-review.yml workflow covers test annotations, not security exceptions).
Emergency Bypass
In emergencies, use the skip-security-check label on PRs:
gh pr edit --add-label skip-security-check
Requirements:
- Explain the reason for the bypass in the PR description
- Create a follow-up issue to track what needs to be fixed
- The PR can be merged only after approval by the Security Team or an Admin
⚠️ Warning: skip-security-check is for emergencies only; abuse will be recorded and reviewed.
Feature Flags
Some security features can be enabled gradually using environment variables:
ENFORCE_PROMPT_OWNERSHIP
Controls prompt template ownership enforcement:
true(default): Enforce ownership checksfalse: Log only, do not reject requests (for gradual rollout)
The one-time gradual rollout plan (phasing, rollback procedure, success criteria) has been moved to docs/dev-notes/2026-09-29-enforce-prompt-ownership-rollout.md.
Security Review Checklist
When reviewing PRs with baseline changes:
- All new suppressions have complete metadata
-
owneris a valid GitHub user/team -
justificationclearly explains the exception -
test_coverageexists and tests the security control -
expires_atis set and not too far in the future -
alternative_controlsare documented and effective - PR description explains why the exception is necessary
Related Documentation
- Issue #1897: API Security Baseline Cleanup
- API Security Scanner
- Baseline Metadata Validator
- Baseline Diff Checker
Contact
For questions about API security exceptions:
- Security team: @security
- Backend team: @team-backend