diff --git a/.gitea/scripts/validate-pr.js b/.gitea/scripts/validate-pr.js index 6629e2b..0362d4b 100644 --- a/.gitea/scripts/validate-pr.js +++ b/.gitea/scripts/validate-pr.js @@ -154,6 +154,12 @@ for (const file of changedFiles) { const pluginId = file.replace('plugins/', '').replace('.json', ''); + // Filename must be prefixed with username + if (!pluginId.startsWith(`${prAuthor}-`)) { + errors.push(`❌ ${file}: Filename must start with your username "${prAuthor}-" (e.g., "${prAuthor}-my-plugin.json")`); + continue; + } + // Check if file was added or removed let isAdded = false; let isRemoved = false; diff --git a/SECURITY.md b/SECURITY.md index e69de29..c11a3cb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -0,0 +1,137 @@ +# Security Model + +## Authentication vs Git Configuration + +### How User Identity Works + +**Question:** Can't I just set my git username to anything and bypass validation? + +**Answer:** No. The validation uses **Gitea account authentication**, not git commit authors. + +#### Git Commit Author (NOT used for validation) +```bash +# This can be set to anything locally +git config user.name "anyone" +git config user.email "anyone@example.com" +git commit -m "My commit" +``` +These values are **NOT** used for plugin ownership validation. + +#### PR Creator (used for validation) +When you create a Pull Request: +1. You must be logged into your Gitea account +2. The PR is created through Gitea's authenticated API +3. `github.event.pull_request.user.login` = your authenticated Gitea username + +**This is what validation checks against.** + +### Example Attack Scenario (Prevented) + +❌ **Attacker tries:** +```bash +# Configure git to impersonate someone +git config user.name "victim" +git config user.email "victim@example.com" + +# Modify victim's plugin +vim plugins/victim-plugin.json +# Change author to "attacker" + +git commit -m "Update plugin" +git push + +# Create PR (must log into Gitea as "attacker" account) +``` + +✅ **What happens:** +1. PR is created by authenticated Gitea user: `attacker` +2. Validation script receives: `prAuthor = "attacker"` +3. Validation checks original plugin from main branch: `original.author = "victim"` +4. Check fails: `"victim" !== "attacker"` +5. **Result:** ❌ `Cannot modify plugin owned by "victim" (you are "attacker")` + +The git commit author is irrelevant - what matters is **who is logged into Gitea when creating the PR**. + +## Security Measures + +### 1. Dual Checkout +```yaml +- Checkout main branch → base/ (trusted validation scripts) +- Checkout PR branch → pr/ (untrusted user submissions) +``` +Validation script always runs from `base/`, never from PR branch. + +### 2. Infrastructure Protection +PRs that modify `.gitea/` directory are automatically rejected. + +### 3. Ownership Verification +All modifications/deletions check **original ownership from main branch**: +```javascript +// Check original plugin from main (base/) +const originalPlugin = readFile('../base/plugins/example.json'); + +// User can only modify if they own the original +if (originalPlugin.author !== prAuthor) { + reject("Cannot modify plugin owned by someone else"); +} +``` + +### 4. Author Field Validation +New plugins must have `author` field matching the PR creator's authenticated Gitea username. + +## What About Collaboration? + +**Q:** What if I want someone else to update my plugin? + +**A:** They cannot submit PRs directly. Options: +1. Transfer ownership by updating the `author` field yourself +2. They fork your plugin repo and create their own plugin entry +3. Add them as collaborators to your plugin repository (not the directory) + +## Trust Model + +### Trusted Components +- Main branch content (validated history) +- Gitea authentication system +- GitHub Actions runner environment +- Validation scripts on main branch + +### Untrusted Components +- PR branch content (user submissions) +- Git commit metadata (names, emails) +- PR descriptions and comments + +### Validation Flow +``` +1. User authenticates to Gitea → Trusted user identity +2. User creates PR → github.event.pull_request.user.login +3. Validation runs from main branch scripts → Trusted code +4. Checks plugin ownership against main → Trusted ownership data +5. Validates PR author matches plugin author → Secure check +``` + +## Reporting Security Issues + +If you find a security vulnerability in the plugin directory system: +1. **Do NOT create a public issue or PR** +2. Contact the repository maintainer directly +3. Provide detailed reproduction steps +4. Allow time for a fix before public disclosure + +## Additional Safeguards + +### Rate Limiting +Gitea's built-in rate limiting prevents spam PR attacks. + +### Branch Protection +The `main` branch is protected: +- Direct pushes disabled +- All changes via PR +- Auto-merge only after validation passes + +### Audit Trail +All changes are tracked in git history: +- Who created the PR (Gitea account) +- What changed (git diff) +- When it was merged (git commit timestamp) +- Why validation passed (CI logs) diff --git a/VALIDATION_RULES.md b/VALIDATION_RULES.md index 18ad351..af97ca3 100644 --- a/VALIDATION_RULES.md +++ b/VALIDATION_RULES.md @@ -14,7 +14,16 @@ All plugin submissions are automatically validated before being merged. PRs must - ❌ Cannot modify `.gitea/` infrastructure files - ⚠️ `plugins/index.json` is auto-generated - don't manually edit it -### 2. File Type +### 2. File Naming +- **Filename must be prefixed with your username** +- Format: `username-plugin-name.json` +- Example: `litruv-example-plugin.json` +- ❌ Invalid: `example-plugin.json` (missing username prefix) +- ❌ Invalid: `otheruser-plugin.json` (wrong username) + +This prevents naming conflicts and makes ownership visible. + +### 3. File Type - ✅ Only `.json` files are allowed in `plugins/` directory - ❌ No other file types (images, scripts, etc. must be in plugin repo) @@ -38,7 +47,8 @@ Every plugin JSON must have these fields: #### `id` (required) - Must match the filename -- Example: `example-plugin.json` → `"id": "example-plugin"` +- Example: `litruv-example-plugin.json` → `"id": "litruv-example-plugin"` +- Must include username prefix matching filename - Lowercase, hyphens allowed, no spaces #### `name` (required) @@ -134,9 +144,15 @@ node .gitea/scripts/validate-pr.js "your-username" "plugins/your-plugin.json" **Fix**: Change `"author": "someone"` to `"author": "you"` ### ❌ ID doesn't match filename +```litruv-my-plugin.json: Plugin ID "different-name" doesn't match filename "litruv-my-plugin.json" ``` -❌ plugins/my-plugin.json: Plugin ID "different-name" doesn't match filename "my-plugin.json" +**Fix**: Change `"id": "different-name"` to `"id": "litruv-my-plugin"` + +### ❌ Filename missing username prefix ``` +❌ plugins/my-plugin.json: Filename must start with your username "litruv-" (e.g., "litruv-my-plugin.json") +``` +**Fix**: Rename file from `my-plugin.json` to `litruv-my-plugin.json` and update the `id` field **Fix**: Change `"id": "different-name"` to `"id": "my-plugin"` ### ❌ Invalid version @@ -155,13 +171,7 @@ node .gitea/scripts/validate-pr.js "your-username" "plugins/your-plugin.json" ``` ❌ plugins/example-plugin.json: Thumbnail validation failed: Thumbnail dimensions 1024x768 exceed 512x512 ``` -**Fix**: Resize your thumbnail to 512×512 or smaller - -## Example Valid Plugin JSON - -```json -{ - "id": "example-plugin", +**Fix**: litruv-example-plugin", "name": "Example Plugin", "version": "1.0.0", "description": "An example plugin demonstrating the plugin system capabilities", @@ -173,6 +183,14 @@ node .gitea/scripts/validate-pr.js "your-username" "plugins/your-plugin.json" } ``` +Filename: `litruv-example-plugin.json"author": "litruv", + "repository": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example.git", + "thumbnail": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example/raw/branch/main/thumbnail.png", + "homepage": "http://synbox.ruv.wtf:8418/litruv/Plugin-Example", + "tags": ["example", "demo"] +} +``` + ## Security Note **Never include sensitive information in plugin JSON files.** These files are public and will be served to all plugin host users. Do not include: