Security
This document covers RevitPy’s security posture: input validation via Pydantic, security-focused linting rules, thread safety patterns, the exception hierarchy, and the CI security pipeline.
Input Validation
Pydantic v2 Validators
All ORM entity types inherit from BaseElement (in revitpy/orm/validation.py), which uses Pydantic v2 with validate_assignment=True. This means every attribute write – not just construction – passes through validation.
Key validation constraints enforced at the model level:
| Entity | Field | Constraint | Rationale |
|---|---|---|---|
BaseElement |
name |
max_length=1000, whitespace stripped |
Prevent oversized strings |
BaseElement |
category |
max_length=255, whitespace stripped |
Prevent oversized strings |
BaseElement |
family_name |
max_length=255 |
Bound string length |
BaseElement |
type_name |
max_length=255 |
Bound string length |
BaseElement |
version |
ge=1 |
Prevent invalid versions |
WallElement |
height, length, width |
gt=0 |
Reject non-positive dimensions |
WallElement |
area, volume |
ge=0 |
Reject negative measurements |
WallElement |
fire_rating |
ge=0, le=4 |
Bound to valid hour range |
RoomElement |
number |
min_length=1, max_length=50, alphanumeric+.-_ only |
Prevent injection via room numbers |
RoomElement |
area |
ge=0 |
Reject negative area |
RoomElement |
temperature |
ge=-50, le=150 |
Fahrenheit range bounds |
RoomElement |
humidity |
ge=0, le=100 |
Percentage bounds |
DoorElement |
width, height |
gt=0 |
Reject non-positive dimensions |
DoorElement |
fire_rating |
ge=0, le=4 |
Bound to valid hour range |
DoorElement |
hand |
pattern=^(Left\|Right)$ |
Regex constraint |
WindowElement |
width, height |
gt=0 |
Reject non-positive dimensions |
WindowElement |
solar_heat_gain |
ge=0, le=1 |
SHGC coefficient range |
WindowElement |
sound_transmission_class |
ge=0, le=100 |
STC rating range |
Custom Validation Rules
The ElementValidator supports runtime-configurable validation rules via ValidationRule objects. Each rule specifies a ConstraintType:
REQUIRED– value must not beNoneor empty string.MIN_VALUE/MAX_VALUE– numeric bounds.MIN_LENGTH/MAX_LENGTH– string length bounds.PATTERN– regex match viare.match().CUSTOM– reserved for user-defined logic.
Rules can be added and removed at runtime via add_custom_rule() and remove_custom_rule().
Type Safety Enforcement
The TypeSafetyMixin class (in validation.py) provides ensure_type_safety(obj, expected_type), which:
- Checks
isinstance(obj, expected_type). - If
objis adictandexpected_typeis aBaseElementsubclass, attemptsmodel_validate()conversion. - If the object is a
BaseElement, runsassert_valid()which raisesORMValidationErroron failure.
Parameter Value Validation
The ParameterValue model (in api/element.py) validates parameter values based on their storage_type:
Doublestorage: attemptsfloat()conversion, raisesValidationErroron failure.Integerstorage: attemptsint()conversion, raisesValidationErroron failure.
Query Input Validation
QueryBuilder validates inputs at the method level:
skip(count)– raisesValueErrorifcount < 0.take(count)– raisesValueErrorifcount <= 0.
The ElementFilter dataclass (in orm/types.py) validates that the operator field is one of a fixed set: eq, ne, lt, le, gt, ge, contains, startswith, endswith, in, not_in, is_null, is_not_null, regex.
Security Linting (Ruff S Rules)
RevitPy’s ruff configuration (in pyproject.toml) includes the S rule set (flake8-bandit security rules):
[tool.ruff.lint]
select = [
"E", "W", "F", "I", "B", "C4", "UP",
"S", # flake8-bandit security rules
]
What the S Rules Check
The flake8-bandit rules flag potential security issues including:
| Rule | Category | Example |
|---|---|---|
S101 |
Assert usage | assert statements (disabled in test files) |
S105, S106 |
Hardcoded passwords | Password strings in code (disabled in test files) |
S110 |
try-except-pass | Silently swallowing exceptions (explicitly ignored project-wide) |
S112 |
try-except-continue | Silently continuing past exceptions (explicitly ignored) |
S311 |
Pseudo-random generators | Weak random number generation (disabled for proof-of-concepts) |
S324 |
Insecure hash functions | Use of MD5/SHA1 for security purposes |
S603 |
subprocess calls | Subprocess invocations (explicitly ignored) |
S607 |
Partial executable paths | Incomplete paths in subprocess (explicitly ignored) |
Per-File Overrides
From pyproject.toml:
[tool.ruff.lint.per-file-ignores]
"tests/**/*" = ["S101", "D", "F401", "E402"]
"**/tests/**/*" = ["S101", "S105", "S106", "D", "F401", "E402"]
"revitpy-package-manager/**/*" = ["F401", "E402", "F403", "F405", "S"]
"proof-of-concepts/**/*" = ["S311"]
Tests are allowed to use assert (S101) and hardcoded credentials (S105, S106) for test fixtures. The package manager subproject has all S rules disabled. Proof-of-concept code is allowed to use pseudo-random generators.
Known Exceptions in Source
The codebase has one annotated security-related suppression:
query_builder.pyline 120:hashlib.md5(plan_str.encode()).hexdigest() # noqa: S324– MD5 is used for query plan hashing (cache key generation), not for cryptographic security. This is a performance optimization where collision resistance is not a security requirement.
Thread Safety
Thread safety patterns are documented in detail in the Performance document. A summary of the security-relevant aspects:
Concurrent Access Protection
All mutable shared state is protected by threading.RLock when thread safety is enabled:
- Entity state in
ChangeTracker– prevents concurrent modification of entity tracking dictionaries. - Cache contents in
CacheManagerandMemoryCache– prevents race conditions during cache read/write/eviction. - Event queue in
EventDispatcher–RLockprotects thedequeused for event queuing. - Metrics counters in
PerformanceOptimizerandCacheStatistics– prevents counter corruption under concurrent access.
Singleton Safety
EventManager uses double-checked locking with a class-level threading.Lock to ensure only one instance is created:
class EventManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Semaphore-Based Rate Limiting
EventDispatcher uses asyncio.Semaphore(max_concurrent_async_handlers) (default 10) to limit the number of concurrently executing async event handlers, preventing resource exhaustion from event storms.
Error Handling Hierarchy
ORM Exception Hierarchy
All ORM exceptions are defined in revitpy/orm/exceptions.py and form a single inheritance chain:
RevitPyException (from api.exceptions)
|
+-- ORMException
|
+-- RelationshipError
+-- CacheError
+-- ChangeTrackingError
+-- QueryError
+-- LazyLoadingError
+-- AsyncOperationError
+-- BatchOperationError
+-- ValidationError
+-- ConcurrencyError
+-- TransactionError (orm)
Each exception carries context-specific fields:
| Exception | Key Fields |
|---|---|
ORMException |
operation, entity_type, entity_id, cause |
RelationshipError |
relationship_name, source_entity, target_entity |
CacheError |
cache_key, cache_operation |
ChangeTrackingError |
entity, property_name, tracking_operation |
QueryError |
query_expression, query_operation, element_count |
LazyLoadingError |
property_name, entity |
AsyncOperationError |
async_operation, task_id |
BatchOperationError |
batch_size, failed_operations, successful_operations |
ValidationError |
validation_errors (dict of field to error list), entity |
ConcurrencyError |
entity, conflicting_changes |
TransactionError |
transaction_id, transaction_state, nested_level |
API Exception Hierarchy
API-level exceptions are defined in revitpy/api/exceptions.py:
RevitPyException
|
+-- RevitAPIError
+-- ConnectionError
+-- ElementNotFoundError
+-- ModelError
+-- ValidationError (api)
+-- PermissionError
+-- TransactionError (api)
Error Propagation Pattern
The codebase follows a consistent pattern for error handling:
- Catch at boundary – exceptions from external sources (Revit API, providers) are caught at the boundary.
- Wrap and re-raise – caught exceptions are wrapped in framework-specific exception types with the original exception as
cause, usingraise ... from e. - Log before raising –
loguru.logger.error()is called before raising, ensuring the error is recorded even if the caller swallows the exception. - Context preservation – each exception carries structured fields (entity IDs, operation names, property names) to aid debugging.
Example from RevitContext.get_by_id():
except Exception as e:
logger.error(f"Failed to get element by ID {element_id}: {e}")
raise ORMException(
f"Failed to get element by ID {element_id}",
operation="get_by_id",
entity_type=element_type.__name__,
entity_id=element_id,
cause=e,
) from e
Transaction Safety
Transaction (in api/transaction.py) implements automatic rollback on failure:
- If an exception occurs during
commit(), all operations rolled back before re-raising. - The
__exit__context manager rolls back on any exception, preventing partial commits. TransactionGroup.commit_all()rolls back all already-committed transactions in the group if any individual commit fails.
CI Security Job
The CI pipeline (.github/workflows/ci.yml) includes a dedicated security job:
security:
name: Security
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies
run: pip install -e ".[dev]" pip-audit
- name: Run pip-audit
run: pip-audit
- name: Run ruff security checks
run: ruff check --select S revitpy/ tests/
What This Checks
-
pip-audit: Scans all installed dependencies for known vulnerabilities using the Python Packaging Advisory Database (PyPA advisory database). Fails the build if any dependency has a published CVE.
-
ruff –select S: Runs only the flake8-bandit security rules against the entire
revitpy/andtests/directories. This catches:- Hardcoded passwords or secrets
- Use of
assertfor access control (in non-test code) - Insecure hash functions used for security
- Subprocess calls with shell injection risk
- Pseudo-random number generators used where cryptographic randomness is needed
try-except-passpatterns that silently swallow errors
Additional CI Security Measures
- Repository permissions: The CI workflow specifies
permissions: contents: read, following the principle of least privilege. - Pinned action versions: All GitHub Actions use specific major versions (
@v4,@v5). - Dependency caching: Uses
cache: pipwithcache-dependency-path: pyproject.tomlfor reproducible builds. - Full history checkout:
fetch-depth: 0enables VCS-based versioning via hatch-vcs.
Type Checking as Security Defense
The type-check CI job runs mypy revitpy with strict settings (from pyproject.toml):
[tool.mypy]
python_version = "3.11"
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_no_return = true
strict_equality = true
While not a security tool per se, mypy catches type-related bugs that could lead to runtime errors or unexpected behaviour, providing an additional layer of defense.
Cloud Token Management and Scope Control
The revitpy.cloud module manages OAuth2 tokens for Autodesk Platform Services (APS) with security controls at multiple levels. For full details see Infrastructure & Cloud.
Token Lifecycle
ApsAuthenticator (in revitpy/cloud/auth.py) handles the OAuth2 client-credentials flow. Security-relevant behaviours:
- Automatic expiry buffer –
ApsToken.is_expiredreturnsTruewhen the token is within 60 seconds of its actual expiry time. This prevents requests from failing due to clock skew or in-flight latency. - No token persistence – tokens are held only in memory (
self._token). They are never written to disk, logged, or serialised. A new token is obtained on each process start. - Minimal scope – the requested scope is
code:all data:write data:read bucket:create, which is the minimum required for Design Automation operations. - Credential isolation –
ApsCredentialsholdsclient_idandclient_secretin a dataclass. The CI/CD templates generated byCIHelperinject these values from environment variables (APS_CLIENT_ID,APS_CLIENT_SECRET) sourced from repository secrets, never from hardcoded values.
Authenticated Request Security
ApsClient (in revitpy/cloud/client.py) injects the Authorization header on every request via get_token(), which triggers re-authentication when the cached token is expired:
token = await self._authenticator.get_token()
headers["Authorization"] = f"{token.token_type} {token.access_token}"
The client enforces a sliding-window rate limit of 20 requests per second to prevent accidental API abuse, and retries only on status codes 429, 500, 502, and 503 – non-retryable errors (such as 401 Unauthorized or 403 Forbidden) propagate immediately as ApsApiError.
HMAC Webhook Signature Verification
WebhookHandler.verify_signature() (in revitpy/cloud/webhooks.py) validates incoming webhook payloads using HMAC-SHA256:
def verify_signature(self, payload: bytes, signature: str) -> bool:
expected = hmac.new(
self._config.secret.encode("utf-8"),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Key properties:
- Uses
hmac.compare_digestfor constant-time comparison, preventing timing side-channel attacks. - Raises
WebhookErrorif called without a configured secret, preventing accidental unverified processing. - The
WebhookConfig.secretis expected to be injected at runtime, not hardcoded.
AI Safety Model
The revitpy.ai module includes a safety system that controls which tools an AI agent may execute against a Revit model. This prevents unintended or destructive modifications when the framework is used in an MCP (Model Context Protocol) context.
Safety Modes
SafetyGuard (in revitpy/ai/safety.py) enforces one of three safety modes, defined by the SafetyMode enum (in revitpy/ai/types.py):
| Mode | Value | Behaviour |
|---|---|---|
READ_ONLY |
read_only |
Blocks all tools in ToolCategory.MODIFY. Only query, analyse, and export tools are allowed. |
CAUTIOUS |
cautious |
Allows all tool categories but flags tools whose category appears in SafetyConfig.require_confirmation_for, indicating the caller should confirm before execution. This is the default mode. |
FULL_ACCESS |
full_access |
Allows all tool categories without confirmation requirements. |
Tool Categories
Each ToolDefinition is assigned a ToolCategory:
| Category | Value | Examples |
|---|---|---|
QUERY |
query |
List elements, get parameters |
MODIFY |
modify |
Create/delete elements, change parameters |
ANALYZE |
analyze |
Run validation, compute metrics |
EXPORT |
export |
Export schedules, generate reports |
Tool Call Validation Pipeline
When the MCP server receives a tools/call request, the following validation pipeline executes inside McpServer._handle_tools_call() (in revitpy/ai/server.py):
-
Tool lookup –
RevitTools.get_tool(tool_name)resolves the tool name to aToolDefinition. Unknown tools return an MCP error response (code-32602). - Safety validation –
SafetyGuard.validate_tool_call(tool, arguments)checks the call against the active policy:- If the tool name appears in
SafetyConfig.blocked_tools, aSafetyViolationErroris raised unconditionally. - In
READ_ONLYmode, any tool withToolCategory.MODIFYraisesSafetyViolationError. - In
CAUTIOUSmode, tools whose category is inrequire_confirmation_forare flagged (logged atINFOlevel).
- If the tool name appears in
-
Execution – if validation passes,
RevitTools.execute_tool(tool_name, arguments)runs the tool and returns aToolResult. - Error response – if
SafetyViolationErroris raised, the server returns an MCP response withisError: trueand the violation message, rather than executing the tool.
SafetyConfig
SafetyConfig (in revitpy/ai/types.py) controls the safety policy:
| Field | Type | Default | Description |
|---|---|---|---|
mode |
SafetyMode |
CAUTIOUS |
Active safety enforcement level |
max_undo_stack |
int |
50 |
Maximum number of undo entries retained |
require_confirmation_for |
list[ToolCategory] |
[] |
Tool categories that require explicit confirmation in CAUTIOUS mode |
blocked_tools |
list[str] |
[] |
Tool names that are always blocked regardless of mode |
Preview and Undo Mechanism
SafetyGuard provides two mechanisms for reversibility:
Preview – preview_changes(tool, arguments) -> dict returns a dry-run summary of what a tool call would do, including the tool name, category, arguments, current safety mode, whether confirmation is required, and whether the tool is blocked. No state is modified.
Undo stack – SafetyGuard maintains a bounded LIFO stack of operations:
push_undo(operation: dict)adds an entry. When the stack exceedsmax_undo_stack(default 50), the oldest entry is discarded.undo_last() -> dict | Nonepops and returns the most recent entry, orNoneif empty.get_undo_stack() -> list[dict]returns a copy of the current stack for inspection.
The undo stack is not automatically populated – callers are responsible for recording reversible operations after successful tool execution.
AI Exception Hierarchy
All AI/MCP exceptions inherit from AiError (in revitpy/ai/exceptions.py):
AiError
|
+-- McpServerError (host, port)
+-- ToolExecutionError (tool_name, arguments)
+-- SafetyViolationError (tool_name, safety_mode, reason)
+-- PromptError (template_name)
SafetyViolationError carries the tool_name, safety_mode, and a human-readable reason string, enabling callers and logs to diagnose exactly why a tool call was denied.
WebSocket Server Security Considerations
McpServer (in revitpy/ai/server.py) exposes tools over a WebSocket connection. Security-relevant aspects:
- Default binding – the server binds to
localhost:8765by default (McpServerConfig), limiting exposure to the local machine. - Connection tracking – active connections are tracked in a
set. Theconnectionsproperty returns a copy to prevent external mutation. - Error isolation – exceptions during message handling are caught per-message and returned as MCP error responses (JSON-RPC error code
-32603). A single malformed message does not terminate the connection. - Graceful shutdown –
stop(timeout=5.0)closes the server and waits for existing connections to drain within the timeout. The connection set is cleared on stop. - Safety integration – every
tools/callrequest passes throughSafetyGuard.validate_tool_call()before execution, ensuring the configured safety policy is enforced regardless of the client. - No built-in authentication – the WebSocket server does not implement its own authentication layer. In production deployments, it should be placed behind a reverse proxy or gateway that handles authentication and TLS termination.