Security
BugDrop is designed with security and privacy at its core. This page covers the permissions the GitHub App requires, how data is stored, privacy guarantees, and the built-in rate limiting that protects against abuse.
GitHub App Permissions
The BugDrop GitHub App requests the minimum permissions necessary to function:
| Permission | Access Level | Purpose |
|---|---|---|
| Issues | Read & Write | Create bug reports, feature requests, and questions as GitHub Issues |
| Contents | Read & Write | Store screenshots in the repository on the bugdrop-screenshots branch |
GitHub App Contents permissions are repository-scoped, not branch-scoped. BugDrop's implementation writes screenshots only to .bugdrop/screenshots/ on the dedicated bugdrop-screenshots branch, but GitHub does not provide a narrower branch-only permission for this API.
BugDrop does not request access to:
- Pull requests
- Actions or workflows
- Secrets or environment variables
- Organization settings
- Collaborators or team membership
- Any other repository or account data
You can review and revoke the app's access at any time from your GitHub settings under Applications > Installed GitHub Apps.
Data Storage
BugDrop follows a "GitHub-native" storage model. All data stays within your GitHub repository -- nothing is stored on external servers.
Issues
Feedback submissions are created as GitHub Issues in your repository. Each issue includes:
- Title and description from the user
- Feedback category (Bug, Feature, or Question) as a GitHub label
- Automatic system information (browser, OS, viewport, language, URL)
- Submitter name and email (if configured and provided)
- Link to the screenshot (if attached)
Screenshots
Screenshots are stored as image files in a .bugdrop/ directory on a dedicated bugdrop-screenshots branch in your repository. This design has several benefits:
- Screenshots do not clutter your main branch -- They live on a separate branch that never merges into your codebase
- Full version history -- Every screenshot is a Git commit, giving you a full audit trail
- GitHub-hosted -- Images are served directly from GitHub, with no external image hosting
- Easy cleanup -- Delete the
bugdrop-screenshotsbranch to remove all screenshots at once
The screenshot branch is created automatically when the first screenshot is uploaded. No manual setup is required.
Treat screenshots as unauthenticated user-generated content. The hosted service enforces rate limits, size limits, and PNG payload validation, but it is not a spam or malware filtering product.
Screenshot Format
Screenshots are captured client-side using html-to-image, which renders the current page to a canvas element in the user's browser. The canvas is then converted to a PNG image and uploaded. This means:
- The initial screenshot capture is rendered from what the user actually sees
- No server-side rendering or page access is required
- The screenshot is generated entirely in the user's browser before being sent to the API
- Users can redact additional screenshot regions before submitting when using the manual screenshot flow
Manual redaction is user-driven and does not automatically detect sensitive content. It complements, but does not replace, developer-configured masking for fields that should be visually covered in supported screenshot modes, especially when using automatic screenshots.
Because clients are untrusted, the API validates screenshot uploads server-side before storing them. BugDrop currently accepts PNG data URLs only and rejects SVG, malformed base64, oversized payloads, and data that does not have a PNG file signature.
Privacy
BugDrop is built with a privacy-first approach:
- No user tracking -- BugDrop does not set cookies, use local storage for tracking, or fingerprint users
- No analytics -- The widget does not send any telemetry, usage data, or analytics to any server
- No external data storage -- All data (issues and screenshots) is stored in your GitHub repository
- No user accounts -- Users submitting feedback do not need to create accounts or log in
- No PII collection by default -- Name and email fields are off by default. When enabled, this data goes only to the GitHub Issue in your repository
- Client-side screenshots -- Screenshots are rendered in the user's browser, not captured server-side
- Open source -- The entire codebase is open source (MIT licensed) and auditable
Screenshot masking
BugDrop supports developer-configured visual masking for screenshots. Add
data-bugdrop-redact or data-bugdrop-mask to any DOM element that should be
covered:
<input type="email" data-bugdrop-redact />
<div data-bugdrop-mask>
<span>Customer name</span>
<span>[email protected]</span>
</div>
Supported explicit attributes are data-bugdrop-redact, data-bd-redact,
data-bugdrop-redacted, and data-bugdrop-mask.
For supported DOM-rendered captures, when masking succeeds, BugDrop records the geometry of matching DOM elements, renders the screenshot, then paints opaque rectangles over those measured boxes in the PNG. The submitted image contains the black rectangles. If masking fails, BugDrop discards the screenshot instead of uploading it. The original page DOM is not mutated.
Masking is visual coverage, not data-loss prevention. BugDrop does not inspect text or pixels to discover secrets. Developers must mark the sensitive regions they control, and users should review manual screenshots before submitting.
Inheritance. When an ancestor has data-bugdrop-mask, the entire ancestor box is
masked as a single rectangle. Descendants do not get individual rectangles — this
prevents gaps from CSS gap or non-masked siblings inside a masked container.
Built-in defaults. In supported DOM-rendered screenshot paths, BugDrop automatically masks these with or without an explicit attribute:
input[type="password"]- Any input with
autocomplete="cc-number",cc-csc, orcc-exp
These defaults do not apply to native viewport capture or skipped/failed screenshot paths.
| Surface | Behavior | Recommended control |
|---|---|---|
| Regular DOM elements | Marked element box is covered | Mark the smallest stable container that fully encloses the sensitive content |
| Password and credit-card inputs | Covered automatically | No extra attribute required, but explicit marks are fine |
| Open Shadow DOM | Traversed when the browser exposes shadowRoot |
Mark inner controls or the host |
| Closed Shadow DOM | Not traversed | Mark the host custom element |
| Iframes | Iframe internals are not traversed | Mark the iframe element if the whole embedded frame is sensitive |
| Canvas, image, SVG, video | Internal pixels are not inspected | Mark the element or a containing wrapper |
| Pseudo-elements and highly custom controls | Generated pixels are not inspected separately | Mark a stable wrapper around the whole control |
| Native viewport capture fallback | Element masks cannot be applied | Avoid viewport fallback on pages that require masking |
If a marked region includes embedded, media, or pixel-rendered content, BugDrop covers the marked element box and warns the user in manual screenshot review flows that it cannot inspect the internal pixels. Automatic screenshot mode still applies supported masks, but it submits without showing a review step.
Mask rectangles are measured at capture start. If the page reflows or reveals sensitive elements after that measurement but before rendering finishes, a mask can become stale. Keep sensitive marked regions stable during capture.
The only network requests BugDrop makes are:
- Loading the widget script from Cloudflare Workers
- Submitting the feedback form to the BugDrop Cloudflare Worker API
The API acts as a pass-through to the GitHub API -- it receives the form data, creates the issue and uploads the screenshot, and discards the data. Nothing is persisted on the Cloudflare Worker.
Rate Limiting
BugDrop includes built-in rate limiting to protect against abuse and ensure fair usage. Rate limits are applied at the Cloudflare Worker API level.
Rate Limit Tiers
| Scope | Limit | Window | Description |
|---|---|---|---|
| Per IP | 10 requests | 15 minutes | Limits individual users from flooding the API |
| Per Repository | 50 requests | 1 hour | Limits total submissions to any single repository |
Both limits are enforced simultaneously. A request must pass both the per-IP and per-repository checks to succeed.
Rate Limit Headers
Every API response includes rate limit headers so you can monitor usage:
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit |
Maximum requests allowed in the window | 10 |
X-RateLimit-Remaining |
Requests remaining in the current window | 7 |
Retry-After |
Seconds until the rate limit resets (only on 429 responses) | 420 |
429 Response Behavior
When a rate limit is exceeded, the API returns an HTTP 429 Too Many Requests response with:
- A JSON body containing an error message explaining which limit was hit
- A
Retry-Afterheader indicating how many seconds to wait before retrying
The widget handles 429 responses gracefully by displaying a user-friendly message in the form. The user is told to try again later and shown approximately how long to wait.
Example 429 response:
{
"error": "Rate limit exceeded. Please try again later.",
"retryAfter": 420
}
Rate Limit Design Rationale
The rate limits are set to be generous enough for legitimate usage while preventing abuse:
- 10 per IP / 15 minutes -- Even an active bug reporter rarely submits more than a few reports in 15 minutes. This limit stops automated scripts and spam while being invisible to real users.
- 50 per repository / hour -- This allows a team of users to submit feedback without hitting limits, while preventing a single repository from being overwhelmed by a flood of submissions.
If these limits are too restrictive for your use case, consider self-hosting BugDrop with custom rate limit configuration.
Security Best Practices
For Site Owners
- Review app permissions -- Periodically check the BugDrop GitHub App's permissions in your GitHub settings
- Monitor the screenshots branch -- Occasionally review the
bugdrop-screenshotsbranch for unexpected content - Exclude screenshot storage from CI -- Do not run privileged CI/deploy workflows on
bugdrop-screenshots; treat it as user-generated content storage, not application source - Use branch protection -- Keep your main branch protected and limit deploy/build workflows to
mainor other trusted branches - Set up CSP -- If you use a Content Security Policy, explicitly whitelist the required domains rather than using broad wildcards
The hosted service is intended for lightweight feedback collection. If your site is public, high-traffic, compliance-sensitive, or exposed to adversarial submissions, self-host BugDrop and place it behind your own WAF, CAPTCHA, logging, retention, and content filtering controls.
For Self-Hosters
If you run your own instance of BugDrop:
- Rotate your GitHub App credentials regularly
- Set appropriate rate limits for your expected traffic
- Monitor your Cloudflare Worker logs for unusual activity
- Add edge protections such as WAF rules, CAPTCHA, bot detection, and allowlists as needed
- Define retention/cleanup for the
bugdrop-screenshotsbranch - Use host-app auth tokens when the widget should only be available to authenticated users
- Keep your instance updated with the latest version
Host-App Auth Tokens
For private or authenticated applications, self-hosted workers can require a signed token on feedback submissions. Set AUTH_TOKEN_SECRET on the BugDrop worker, mint short-lived tokens from your own backend after checking the user's session, and configure the widget with data-auth-token-provider.
When rotating secrets or sharing one worker across multiple authenticated host apps, keep the current AUTH_TOKEN_SECRET in place and add comma- or newline-separated verifier secrets with AUTH_TOKEN_ADDITIONAL_SECRETS. BugDrop accepts tokens signed by any configured secret, so one app can migrate without breaking another app that still signs with the primary secret.
This protects the feedback endpoint from direct API calls that bypass browser CORS. CORS still matters for browser isolation, but it is not an authentication boundary. The token should include the exact target repository and expire quickly, usually within 5 minutes.
If installation status is also sensitive, set AUTH_TOKEN_REQUIRED_FOR_CHECK = "true" so the widget's /check/:owner/:repo request requires the same token.
Reporting Security Issues
If you discover a security vulnerability in BugDrop, please report it responsibly:
- Do not create a public GitHub Issue for security vulnerabilities
- Contact the maintainers directly through GitHub's private vulnerability reporting feature on the BugDrop repository
Next Steps
- Install BugDrop on your site
- Self-host BugDrop for full control
- View the FAQ for common questions