Skip to content

Security & Rate Limiting

CourseWeave has two browser-facing endpoints that accept user input:

  • Report Issue / Session Reflection β€” student-facing, posts to a Cloudflare Worker β†’ GitHub Issues
  • Content Creator (Alt+C) β€” teacher-facing, posts to a Cloudflare Worker β†’ GitHub Issues

Both sit behind a layered defence: client-side rate limiting, a honeypot field, and a server-side backstop at the Cloudflare Worker.


IP-based rate limiting is the default in most tutorials, but it breaks in a university context: all students on campus or in a dormitory typically share one or a few NAT’d IP addresses. One student submitting ten feedback items would block the entire cohort for an hour.

Cloudflare Turnstile / reCAPTCHA were also considered and rejected:

  • reCAPTCHA is blocked in China.
  • Cloudflare Turnstile’s challenge endpoint (challenges.cloudflare.com) is reachable from mainland China in most cases but is not guaranteed reliable across all campus networks.
  • A CAPTCHA that silently fails blocks the form entirely. Students behind university proxies or VPNs (common in China) are the most likely to trigger false positives.

For a course feedback form, the threat model does not justify that failure mode.


Rate limits are enforced in the browser using localStorage, keyed per device. Timestamps older than 24 hours are pruned on each check so storage never accumulates.

FormKeyLimit
Report Issue (general feedback)courseweave_rl10/hour, 30/day
Session Reflection (CIQ)courseweave_rl (same log, category: 'reflection')2/day
Content Creatorcourseweave_cc_rl20/hour, 50/day

When a limit is exceeded:

  • Report Issue β€” the form switches to the error panel with a friendly explanation.
  • Content Creator β€” a toast notification appears in the top-right corner.

The limits are defined as named constants at the top of each component’s <script> block, making them straightforward to adjust:

ReportIssue.astro
const RL_LIMIT_HOUR = 10;
const RL_LIMIT_DAY = 30;
const RL_LIMIT_CIQ_DAY = 2;
// ContentCreatorBar.astro
const CC_RL_LIMIT_HOUR = 20;
const CC_RL_LIMIT_DAY = 50;

Both forms include a visually hidden input that is never filled by a real user but will be filled by simple bots that blindly populate all fields. If the honeypot field is non-empty on submission, the request is silently dropped (for bots) or a fake success is shown (for the Report Issue form, to avoid tipping off scrapers).

<!-- Hidden from real users, visible to bots -->
<input type="text" name="website" tabindex="-1"
style="position: absolute; left: -9999px;" autocomplete="off" aria-hidden="true" />

The client-side limits are the primary guard. The Cloudflare Worker provides a high-threshold backstop against scripted abuse that bypasses the browser entirely (e.g., direct curl requests to the worker URL).

Recommended Worker configuration:

SettingValueReason
IP rate limit200–500 requests/hourHigh enough that a legitimate classroom session never hits it; low enough to block sustained scripted attacks
Allowed originsYour site’s domain onlyRejects requests not originating from the course site
Secret header (optional)Static token in config.tsCheaply blocks direct API abuse without user friction

The Worker rate limit is intentionally set high because the client-side limits handle the normal case. Lowering the Worker limit risks re-introducing the shared-IP problem the client-side approach was designed to avoid.


Real user β†’ honeypot check β†’ localStorage rate limit β†’ Cloudflare Worker β†’ GitHub Issues API
Bot β†’ honeypot check β†’ silently dropped / fake success
Scripter β†’ (bypasses browser) β†’ Cloudflare Worker IP limit β†’ blocked

No CAPTCHA, no IP blocking of shared addresses, no dependency on services blocked in China.

Current page
πŸ€–