A3lix Security Documentation
This document describes the security architecture of A3lix. If you discover a vulnerability, please follow the Reporting Vulnerabilities process — do not open a public GitHub issue.
Table of Contents
- Threat Model
- GitHub Token Scoping
- Path Restrictions
- Destructive Keyword Blocklist
- Rate Limiting
- Approval Flow
- Role Enforcement
- OTP Whitelist Flow
- Audit Log
- Webhook Security
- Update System Safety
- Security Checklist
- Reporting Vulnerabilities
Threat Model
A3lix sits between public Telegram users and your private GitHub repository. The primary threat surface is:
| Threat | Mitigation |
|---|---|
| Unauthorised user sends messages | All incoming Telegram chat IDs are checked against agent.json role lists; unknown IDs receive no response and are logged |
| Authorised user requests destructive change | Destructive keyword blocklist rejects requests containing dangerous patterns before AI processing |
| AI hallucinates a path outside allowed directories | guardrails.ts performs a hard path check on every proposed file write — no exceptions |
| Secrets leaked via git | agent.json and .dev.vars are in .gitignore; npx a3lixcms@latest update never touches them |
| Compromised GitHub token used to deploy malware | Fine-grained PAT is scoped to a single repo and only Contents + Workflows permissions |
| Webhook spoofing | Every inbound Telegram webhook is verified against X-Telegram-Bot-Api-Secret-Token |
| Replay or flood attacks | KV-backed per-user rate limiting rejects excess requests within a 24-hour window |
| Accidental production changes | Use PREVIEW mode for human review before merge; restrict editor access; monitor audit logs |
GitHub Token Scoping
A3lix uses a GitHub Fine-Grained Personal Access Token (not a classic PAT). This is mandatory — classic PATs grant too much access.
Required permissions (nothing else)
| Permission | Access level | Reason |
|---|---|---|
| Contents | Read & Write | Read existing files; push preview branches; merge approved branches |
| Workflows | Read & Write | Required if the repo uses GitHub Actions for builds or deployments |
How to create the token
- Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
- Click Generate new token
- Set Resource owner to the account or organisation that owns the repo
- Under Repository access, choose Only select repositories and pick your site repo
- Under Permissions, expand Repository permissions and set:
Contents→ Read and writeWorkflows→ Read and write- Leave everything else at No access
- Click Generate token, copy it, and store it with
wrangler secret put GITHUB_TOKEN
⚠️ Never use a classic PAT. Classic PATs cannot be scoped to a single repository and grant access to all your repos.
Path Restrictions
The guardrails.ts module (in worker/src/) enforces an allowlist of paths that the agent is permitted to read or write. The allowlist is loaded from agent.json at runtime:
paths.allowed:
- src/content
- src/components
- src/pages
- public
- tailwind.config.*
How enforcement works
- After the AI parses the user request and produces a list of proposed file operations, each target path is passed to
validatePath(filePath, allowedPaths). validatePathresolves the path relative to the repository root and checks that it begins with one of the entries inpaths.allowed.- Path traversal sequences (
../,%2e%2e, URL-encoded variants) are detected and immediately rejected. - If any path fails validation, the entire request is aborted — no partial changes are made. The user receives: "That request would modify files outside the allowed paths. Please contact your site owner to expand the allowed paths list."
- The violation is written to the Audit Log with the offending path.
Expanding the allowlist
Only the site owner can modify agent.json. Before adding a path, consider:
- Is it a path that could expose application logic or secrets? (e.g.
src/lib,src/env.ts— do not add) - Could a change to this path break the build or compromise security? If unsure, keep the defaults.
Destructive Keyword Blocklist
A3lix runs a pattern-matching pre-filter on every incoming request before it is sent to the AI. Requests containing any of the following patterns are rejected immediately with the message: "That request contains prohibited keywords and cannot be processed."
| Pattern | Reason blocked |
|---|---|
delete |
Ambiguous destructive intent |
drop |
SQL/DB destruction |
rm -rf |
Shell file deletion |
truncate |
File/DB truncation |
format |
Disk/storage format |
wipe |
Mass deletion |
destroy |
General destructive term |
__secret |
Secret variable naming convention |
.env |
Environment file reference |
process.env |
Node.js secret access pattern |
import.meta.env |
Vite/Astro secret access pattern |
GITHUB_TOKEN |
GitHub token reference |
TELEGRAM_BOT_TOKEN |
Telegram token reference |
eval( |
Code injection |
<script |
XSS injection |
javascript: |
URL-based XSS |
data:text/html |
Data URI injection |
The blocklist is case-insensitive. All blocked requests are logged to the audit log with the matched pattern.
Note: This blocklist is a defence-in-depth measure, not the primary security control. The AI itself is also instructed via system prompt to refuse destructive requests, and path restrictions provide a final backstop.
Rate Limiting
Rate limits are enforced per Telegram chat ID using Cloudflare KV, which provides globally consistent counters with TTL-based expiry.
How it works
- On each request, the worker reads the key
rate:<chatId>:<YYYY-MM-DD>from KV. - If the value is absent, the counter is initialised to
0with a TTL of86400seconds (24 hours), automatically expiring at midnight UTC. - If the counter is ≥
limits.changesPerUserPerDay(fromagent.json), the request is rejected: "You've reached your daily limit of X change requests. Resets at midnight UTC." - Otherwise, the counter is incremented and the request proceeds.
Default limits
| Limit | Default | Field in agent.json |
|---|---|---|
| Changes per user per day | 5 |
limits.changesPerUserPerDay |
| Preview branch expiry | 24 hours |
limits.previewExpiryHours |
Pending preview approvals expire from KV after previewExpiryHours (and are also filtered by expiresAt) to prevent stale approval prompts.
Approval Flow
A3lix currently supports two deployment paths:
- A requester sends a change request.
- The worker parses and validates the proposed file changes.
- The requester is prompted to choose
LIVEorPREVIEW. - If
LIVEis chosen, changes are committed directly to the base branch. - If
PREVIEWis chosen, a preview branch is created and a Pages preview URL is returned. - For previews, replying
YESmerges tomain; replyingNOdiscards the pending approval record.
Safety guidance: Treat
PREVIEWas the recommended production workflow so a human can validate changes before merge.
Role Enforcement
Roles are enforced at the Worker level on every request, not just at the Telegram bot level.
| Role | Can request changes | Can approve changes | Can manage roles | Can view audit log |
|---|---|---|---|---|
| Owner | ✅ | ✅ | ✅ | ✅ |
| Editor | ✅ | ✅ (for their own pending preview) | ❌ | ❌ |
| Viewer | ❌ | ❌ | ❌ | ❌ |
Key enforcement rules
- Owner can always act on pending previews. The owner may approve/reject previews regardless of who requested them.
- Requester can act on their own pending preview. Editors can approve/reject their own preview submission.
- Viewers are strictly read-only. Any command from a viewer that would cause a write operation is rejected.
- Unknown chat IDs receive no response. The worker returns HTTP 200 to Telegram (to prevent retries) but takes no action and logs the unknown ID.
OTP Whitelist Flow
New users are onboarded via a one-time password (OTP) challenge to prevent unauthorised users from adding themselves as editors or viewers.
Step-by-step flow
- The site owner runs
/addeditoror/addviewerin the Telegram bot, which generates a 6-digit OTP and stores it in KV with a 10-minute TTL. - The owner shares the OTP out-of-band with the new user (e.g. via email or in person).
- The new user messages the bot:
/join <OTP> - The worker looks up the OTP in KV:
- Valid and not expired: The user's Telegram chat ID is added to the appropriate role list. The OTP is deleted from KV immediately (single-use). The owner receives a confirmation notification.
- Invalid or expired: The request is rejected. The failed attempt is logged with the chat ID and timestamp.
- OTPs cannot be reused. A new
/addeditoror/addviewercommand generates a fresh OTP.
Note: In v0.1, the role list is stored in KV and synced back to
agent.jsonby the setup CLI. Modifyingagent.jsonmanually and redeploying also works.
Audit Log
Every significant action is written to Cloudflare KV under the key prefix audit:<timestamp>:<chatId>. Log entries are JSON objects:
{
"timestamp": "2026-03-08T14:23:01.000Z", // ISO 8601 UTC
"chatId": "123456789", // Telegram chat ID of actor
"role": "editor", // Role at time of action
"action": "CHANGE_REQUESTED", // Action type (see below)
"filePaths": ["src/content/blog/post.md"], // Files affected (if applicable)
"branch": "preview-update-hero-abc123", // Branch name (if applicable)
"approved": null, // null | true | false
"metadata": {} // Additional context
}
Action types
| Action | Triggering event |
|---|---|
CHANGE_REQUESTED |
User submits a change request |
CHANGE_APPROVED |
Owner or requester sends YES on a pending preview |
CHANGE_REJECTED |
Owner or requester sends NO on a pending preview |
CHANGE_BLOCKED_PATH |
Request blocked by path guardrail |
CHANGE_BLOCKED_KEYWORD |
Request blocked by keyword filter |
RATE_LIMIT_HIT |
User exceeds daily change limit |
UNKNOWN_USER |
Message received from unknown chat ID |
OTP_ISSUED |
Owner issued an OTP for a new user |
OTP_REDEEMED |
New user redeemed an OTP successfully |
OTP_FAILED |
Invalid or expired OTP attempt |
APPROVAL_SPOOFED |
Unauthorized user attempted to approve/reject a pending preview |
Audit log entries are retained in KV for 30 days, after which they expire automatically via KV TTL. The owner can query recent logs with the /audit bot command.
Webhook Security
Telegram webhooks are authenticated using the X-Telegram-Bot-Api-Secret-Token header:
- During setup, a cryptographically random 256-bit string is generated and stored as the
TELEGRAM_SECRET_TOKENwrangler secret. - When registering the webhook with Telegram's
setWebhookAPI, thesecret_tokenparameter is set to this value. - On every inbound webhook request, the worker reads the
X-Telegram-Bot-Api-Secret-Tokenheader and compares it toTELEGRAM_SECRET_TOKENusing a constant-time comparison to prevent timing attacks. - If the header is absent or does not match, the worker returns HTTP 401 immediately — no processing occurs.
This ensures that only Telegram (which knows the secret token) can trigger the worker, even if an attacker knows your worker's public URL.
Update System Safety
Running npx a3lixcms@latest update fetches and applies the latest worker code from the npm registry. The update system is designed to be non-destructive:
Files the updater will NEVER touch
agent.json— your configuration.dev.vars— your local secrets- Any wrangler secrets set via
wrangler secret put— these live in Cloudflare's encrypted secrets store, not in the filesystem worker/wrangler.toml— your KV namespace IDs and binding names
Files the updater WILL replace
worker/src/**/*.ts— the worker source codedeployers/**/*.ts— deployer interface stubssetup/**/*.ts— the CLI setup scripts
The updater always shows a diff of what will change and asks for confirmation before applying any update.
Security Checklist
Run through this checklist before going live with A3lix:
-
agent.jsonis in.gitignoreand not committed to the repository -
.dev.varsis in.gitignoreand not committed to the repository - GitHub PAT is a fine-grained token scoped to only the target repo with only Contents + Workflows permissions
-
GITHUB_TOKENis stored as a wrangler secret (wrangler secret put GITHUB_TOKEN), not inwrangler.tomlvars -
TELEGRAM_BOT_TOKENis stored as a wrangler secret, not inwrangler.tomlvars -
TELEGRAM_SECRET_TOKENis set and the webhook was registered withsecret_tokenparameter -
bot.ownerChatIdis set to your Telegram chat ID (not a group or channel) - Team members use
PREVIEW(notLIVE) for production changes requiring human verification -
paths.allowedcontains only the minimum paths needed (do not addsrc/lib,src/env.ts, etc.) - You have reviewed the
roles.editorsandroles.viewerslists and removed any stale chat IDs - Workers AI (or your chosen AI provider) is enabled and the API key (if required) is stored as
AI_API_KEYsecret - KV namespace IDs in
wrangler.tomlare set to real IDs (not the placeholder values) - You have sent a test message to the bot and confirmed the audit log is being written to KV
- You have tested the
NOpreview flow and confirmed pending approval state is removed
Reporting Vulnerabilities
A3lix takes security seriously. If you discover a vulnerability, please do not open a public GitHub issue.
Instead, send details to: security@a3lix.com
Please include:
- A description of the vulnerability and its potential impact
- Steps to reproduce (proof-of-concept code or screenshots are helpful)
- Any suggested mitigations you have in mind
We will acknowledge your report within 48 hours and aim to release a fix within 14 days for critical issues. We follow coordinated disclosure: we will notify you before publishing any fix so you can review the patch.
We do not currently have a formal bug bounty programme, but we will publicly credit researchers (with your consent) in the release notes.