This document defines the security requirements and boundaries for StudyTube.
StudyTube is a self-hosted application designed primarily for individual learners.
Security priorities are:
- Protect learner data
- Protect server credentials
- Prevent unauthorized database access
- Validate untrusted input
- Prevent common web vulnerabilities
- Keep external integrations isolated
- Fail safely
- Keep the security model simple enough to maintain
StudyTube follows a simple principle:
Treat everything crossing a trust boundary as untrusted.
This includes:
- Browser input
- Form submissions
- URL parameters
- YouTube API responses
- HTTP headers
- Environment configuration
- Imported data
- Database values
- External network responses
The application must validate and normalize data before using it.
The primary security boundaries are:
Browser
│
│ untrusted input
▼
Next.js Server
│
├── SQLite
│
└── YouTube API
The browser must never bypass the server to access:
- SQLite
- Server filesystem
- YouTube API credentials
- R2 credentials
- Server environment variables
The browser is not trusted.
Client-side validation is useful for user experience but is not a security boundary.
For example:
Browser validation
does not replace:
Server validation
Every mutation must validate its inputs on the server.
The server is authoritative for:
- Database mutations
- Course structure
- Module structure
- Lesson structure
- Ordering
- Progress
- Notes
- YouTube API access
- Profile
The client may request an operation.
The server decides whether that operation is valid.
The browser must never connect directly to SQLite.
Incorrect:
Browser
↓
SQLite
Correct:
Browser
↓
Next.js
↓
SQLite
This prevents exposing the database file or database interface to the public network.
The SQLite database contains important learner data.
It must:
- Exist on server-side storage
- Not be served as a static asset
- Not be placed in the public directory
- Not be directly downloadable
- Not be exposed through a route
- Have appropriate filesystem permissions
The database should reside outside the application's publicly served files.
SQLite does not require a database username/password in the traditional sense.
Its primary security boundary is filesystem access.
Therefore:
Protect the filesystem
is equivalent to:
Protect the database
The application process should have only the permissions it requires.
Database paths must come from trusted server configuration.
Do not allow browser input to determine arbitrary filesystem paths.
Never construct database paths directly from request parameters.
Incorrect:
/database?path=<user supplied path>
The application must use a fixed/configured database location.
Secrets must be supplied through environment variables or an equivalent secure secret-management mechanism.
Examples include:
YOUTUBE_API_KEY
and, when backups are enabled:
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
Secrets must not be hard-coded into source code.
Only values explicitly intended for browser use may be exposed to client-side code.
Sensitive server variables must never use a client-exposure mechanism such as a public environment-variable prefix.
The following must remain server-only:
YOUTUBE_API_KEY
R2 credentials
Database configuration
Backup credentials
Never log secrets.
Avoid logging:
API keys
Access tokens
Passwords
Secret environment variables
Authorization headers
Signed URLs containing credentials
Logs should contain enough information for troubleshooting without exposing credentials.
The repository must not contain:
Real API keys
Real cloud credentials
Production database files
Private backup files
Secrets in test fixtures
Sensitive local files should be excluded through .gitignore.
Development environment files containing real credentials must not be committed.
For example:
.env.local
.env.production
should remain local or be managed by the deployment platform.
The repository may contain:
.env.example
with placeholder values.
All external input must be validated.
Examples:
Course title
Course description
Course learning goal
Module title
Lesson ID
Course ID
Module ID
YouTube video ID
Playback position
Note content
Ordering positions
Zod should be used where appropriate for structured validation.
Validation should occur at the server boundary.
For example:
Request
↓
Parse
↓
Validate
↓
Business rule checks
↓
Database mutation
Do not perform a database mutation before validation is complete.
V1 has no authentication or multi-user authorization.
The single-row local profile table (see DATA_MODEL.md §44) is a name for the installation. It provides no access control and must never be mistaken for authorization.
Therefore, the application assumes:
Anyone with access to the deployed StudyTube instance can use and modify that instance.
This is an important deployment assumption.
StudyTube must not falsely imply that V1 provides user accounts or access control.
Because V1 has no authentication, operators who need restricted access should place StudyTube behind an appropriate external access-control layer.
Examples may include:
- Private network
- VPN
- Reverse-proxy authentication
- Hosting-provider access control
- Other infrastructure-level authentication
These mechanisms are outside the core V1 application.
Do not create an internal authentication system merely to make the application appear secure.
If authentication is not implemented:
No authentication
must remain the documented behavior.
The local profile is not authentication. A profile name must never be treated as a credential, identity, or access-control signal.
Authentication is a future feature, not a hidden assumption.
State-changing operations must be designed with browser request security in mind.
Where framework-provided protections apply, use them rather than creating custom security mechanisms unnecessarily.
Server Actions should use Next.js's built-in request model appropriately.
For custom Route Handlers:
- Validate request methods
- Validate request bodies
- Avoid accepting unintended cross-origin requests
- Apply appropriate origin/security checks where necessary
StudyTube is primarily designed as a same-origin web application.
For example:
https://study.example.com
serves both:
UI
and:
Application endpoints
Avoid unnecessary cross-origin APIs.
StudyTube should not enable permissive CORS by default.
Avoid configurations such as:
Access-Control-Allow-Origin: *
unless a documented requirement actually exists.
V1 does not require a public cross-origin API.
Database queries must use Drizzle's parameterized query mechanisms.
Never construct SQL statements by concatenating raw user input.
Incorrect:
"SELECT * FROM lessons WHERE title = '" + userInput + "'"
Correct:
Parameterized query
The ORM/query builder should handle parameter binding.
User-controlled values must not determine arbitrary SQL fragments.
For example, a user should not be able to provide raw:
ORDER BY
WHERE
TABLE
COLUMN
expressions unless explicitly validated against an allowlist.
User-controlled content may include:
- Course titles
- Descriptions
- Notes
- YouTube metadata
These values must be rendered as text by default.
Do not inject user content into raw HTML unless absolutely necessary.
Avoid:
dangerouslySetInnerHTML
for ordinary learner content.
If rich HTML becomes a future requirement, it must be sanitized with a well-maintained sanitizer and an explicitly defined allowed-content policy.
Lesson notes are user-controlled data.
Notes must be stored and rendered safely.
The application should treat note content as plain text in V1.
A learner entering:
<script>alert("test")</script>
must not cause JavaScript to execute when the note is displayed.
YouTube API responses are external data.
Even though they originate from a trusted platform, StudyTube should treat them as untrusted input.
Validate:
- Video ID
- Title
- Channel ID
- Channel name
- Thumbnail URL
- Duration
- Description
- Published timestamp
before storing or displaying them.
The YouTube API key must only be used from server-side code.
Architecture:
Browser
↓
StudyTube Server
↓
YouTube API
Never:
Browser
↓
YouTube API with StudyTube secret
Search endpoints should not allow unlimited uncontrolled requests.
StudyTube should:
- Require an explicit search action
- Validate search parameters
- Avoid search-on-every-keystroke
- Limit unreasonable query sizes
- Handle API quota failures safely
If rate limiting becomes necessary, it can be introduced at the application or infrastructure layer.
StudyTube should store the canonical YouTube video ID rather than trusting arbitrary URLs as persistent identifiers.
Example:
youtube_video_id
is preferred over storing an uncontrolled URL as the primary reference.
When accepting URLs, parse and validate them before extracting a video ID.
Stored external URLs should not automatically be treated as safe.
Where a URL is displayed as a link, the application should use appropriate URL validation.
For known YouTube resources, restrict URLs to expected YouTube domains where practical.
Do not allow user input to determine arbitrary redirect destinations.
Avoid patterns such as:
/redirect?url=<arbitrary URL>
unless the destination is validated against a strict allowlist.
StudyTube does not require arbitrary external redirects in V1.
The YouTube player is external content.
The application should embed it through the supported YouTube player mechanism rather than attempting to download or proxy the video.
The player should not receive StudyTube server secrets.
Production deployments use the browser security headers below, configured in next.config.ts.
Content-Security-Policy
X-Content-Type-Options
Referrer-Policy
Permissions-Policy
X-Frame-Options
The policy explicitly accounts for required YouTube embedding and application behavior.
Do not deploy an overly restrictive policy that breaks the core learning experience.
StudyTube ships a pragmatic Content-Security-Policy header that covers the required StudyTube resources.
The policy:
- Allows the application origin, YouTube's player script (
www.youtube.com,s.ytimg.com), YouTube player frames (youtube.com,youtube-nocookie.com), thumbnails (*.ytimg.com), and inline styles. - Uses
'unsafe-inline'for scripts to stay compatible with Next.js RSC hydration; this is a deliberate pragmatic trade-off and should not be relied on as the only injection defense. - Denies framing (
frame-ancestors 'none'), objects, and unsupported base URIs.
Do not use:
*
as a blanket security policy.
The CSP is tested against Learning Mode and YouTube playback in the manual browser pass.
StudyTube sends X-Frame-Options: DENY to prevent unauthorized framing of the application.
The application does not allow arbitrary third-party sites to embed the StudyTube UI.
The application prevents browsers from incorrectly interpreting returned content as another MIME type by sending:
X-Content-Type-Options: nosniff
Errors crossing the browser boundary must be sanitized.
Do not return:
Database stack trace
Filesystem path
Environment variable
Internal SQL query
Third-party API credential
Instead return a safe application-level error.
Detailed diagnostic information may be logged server-side when appropriate.
However, logs should still avoid sensitive values.
For example:
YouTube search failed
is acceptable.
Including the complete API key in the log is not.
If authentication is introduced later, it must be designed as a major architectural change.
Future authentication would require:
Users
Ownership
Authorization
Session management
Password/token security
Migration strategy
It must not simply be bolted onto UI components.
If multi-user support is introduced, every resource must have an ownership or authorization relationship.
For example:
User
└── Course
└── Module
└── Lesson
Queries must verify ownership at the server boundary.
A future user must never be able to access another user's:
- Courses
- Lessons
- Notes
- Progress
by changing an ID in a request.
Even without authentication in V1, resource IDs should not be treated as an authorization mechanism.
For future authenticated deployments:
Knowing a course ID
must never imply:
Permission to access that course
Authorization must be checked independently.
Endpoints should avoid accepting unnecessarily large request bodies.
For example:
Course title
Module title
Note content
Search query
should have reasonable maximum lengths.
This reduces abuse and prevents accidental oversized requests.
Notes should have a documented reasonable maximum length.
The exact limit may be chosen during implementation.
The important principle is:
Unlimited request body
should not be assumed.
YouTube search input should have a reasonable length limit.
Reject or normalize obviously excessive queries.
This prevents unnecessary external API requests and reduces abuse.
Playback position is numeric input from the browser.
The server must validate:
Type
Non-negative value
Reasonable range
If duration is known:
position <= duration
should be enforced or safely clamped.
Reordering requests must validate that the requested items actually belong to the expected parent.
For example:
Move lesson X within module Y
must not accidentally modify:
Lesson Z in module Q
because a malicious request supplied arbitrary IDs.
Every mutation involving nested resources should verify relationships.
Example:
Course A
└── Module A
└── Lesson A
A request claiming:
course_id = B
module_id = A
must not be accepted simply because both IDs are valid.
The server must verify the actual relationship.
Future authenticated versions must specifically protect against Insecure Direct Object Reference vulnerabilities.
For example:
/api/courses/123
must not expose course 123 merely because the requester knows its ID.
Authorization must be evaluated against the authenticated user.
V1 does not support file uploads.
Therefore, StudyTube does not need to implement:
- Multipart upload security
- File-type detection
- Malware scanning
- Uploaded-file storage
- File execution prevention
If uploads are added later, they require a separate security design.
StudyTube does not host downloaded YouTube videos.
Therefore, it does not require a media-upload security layer in V1.
This significantly reduces the attack surface.
If R2 backups are enabled:
- Credentials remain server-side
- Bucket access should be private
- Backup objects should not be publicly readable
- Credentials should have minimum required permissions
- Backup URLs should not be exposed unnecessarily
The application should never expose the R2 bucket as a public media store.
Backups contain learner data.
Protect them as carefully as the live database.
A backup should not become an accidental public download.
Operators should be able to replace backup credentials without changing application data.
Credentials should be supplied through environment configuration rather than stored inside the SQLite database.
Dependencies are part of the application's attack surface.
StudyTube should:
- Keep dependencies reasonably current
- Review security advisories
- Avoid unnecessary dependencies
- Remove unused packages
- Commit the lockfile
- Review major dependency upgrades
Do not add a dependency solely to solve a trivial problem that can safely be solved with the existing stack.
Dependency updates should be tested before deployment.
At minimum:
Update dependency
↓
Install
↓
Typecheck
↓
Tests
↓
Build
Significant framework or ORM upgrades require additional review.
Do not install packages solely because a random code snippet recommends them.
Before adding a dependency, consider:
- Maintainer reputation
- Maintenance activity
- Package purpose
- Dependency count
- Security history
- License compatibility
- Whether the existing stack already solves the problem
The production container should run with the minimum privileges required.
Where practical:
- Avoid privileged containers
- Avoid unnecessary host mounts
- Avoid host networking unless required
- Avoid exposing unnecessary ports
- Run as a non-root user where compatible with the application
- Keep the image minimal
The application should not require broad filesystem access.
Ideally:
Application
├── Read application files
└── Read/write designated data directory
It should not need unrestricted access to the host filesystem.
Secrets should not be baked into Docker images.
Avoid:
ENV YOUTUBE_API_KEY=...
inside the Dockerfile for real credentials.
Supply secrets at runtime through the deployment environment.
Production images should:
- Use a supported base image
- Minimize unnecessary packages
- Avoid development tools
- Avoid embedded secrets
- Be rebuilt when important base-image security updates are released
Backup operations must not expose the live database through an HTTP endpoint.
Do not create a route such as:
/download-backup
without strong authentication and authorization.
V1 does not require public backup management endpoints.
The application sends the following security headers (configured in next.config.ts):
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: DENY
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy
The header set is verified against the deployed application, including YouTube playback and application behavior.
Production deployments should use HTTPS.
Sensitive configuration should never be transmitted over an unencrypted public connection.
Operators should configure TLS through their reverse proxy or hosting platform.
Local development may use:
http://localhost
This does not represent the production security posture.
Production deployments should use HTTPS.
A security failure should fail closed where practical.
For example:
Invalid request
should result in:
Rejected
rather than:
Best-effort database mutation
If an operation cannot be performed safely, it is better to reject it than to create uncertain state.
Examples:
Invalid lesson relationship
→ Reject
Invalid reorder request
→ Reject
Malformed YouTube response
→ Reject/ignore safely
Invalid playback position
→ Reject or clamp safely
Security-sensitive behavior must have automated tests where practical.
Tests should cover:
Input validation
SQL parameterization
XSS-safe rendering
Invalid resource relationships
Playback validation
Ordering validation
YouTube API failure
Secret exposure prevention
Safe error responses
Security bugs should result in regression tests.
Example:
Bug:
Lesson mutation accepted a lesson from another module.
Fix:
Validate parent relationship.
Regression test:
Cross-module lesson mutation is rejected.
Before a production release:
[ ] No secrets committed
[ ] No secrets exposed to browser
[ ] Database not publicly accessible
[ ] Input validation active
[ ] SQL queries parameterized
[ ] User content rendered safely
[ ] YouTube API key server-side
[ ] R2 credentials server-side
[ ] Safe error messages
[ ] Production HTTPS enabled
[x] Security headers reviewed
[ ] Docker image reviewed
[ ] Dependencies reviewed
[ ] Backup storage private
[ ] Critical security tests passing
The primary V1 threats include:
Browser input abuse
SQL injection
XSS
Credential exposure
Database exposure
Malicious API requests
YouTube API abuse
Unsafe external data
Container misconfiguration
Backup exposure
Dependency vulnerabilities
The V1 architecture intentionally reduces the attack surface by avoiding:
Authentication
Multi-user permissions
File uploads
Video hosting
Public API
Microservices
Distributed infrastructure
StudyTube V1 should be understood as:
A self-hosted application
for a trusted deployment environment
without built-in user authentication.
The single-row local profile (a name for the installation) is covered by this boundary: it is not an account and provides no access control.
This is a deliberate product limitation.
Operators requiring internet-wide private access should add an appropriate external access-control layer.
Potential future requirements include:
- Authentication
- User accounts
- Session management
- Passwordless login
- Multi-user authorization
- Course ownership
- Sharing permissions
- File upload security
- Rate limiting
- Audit logs
- Account recovery
- Multi-device synchronization
These should be treated as new security domains rather than assumed extensions of V1.
Course import accepts a JSON file uploaded by the learner. It is a V1 feature with the following security posture:
- The file is read and validated entirely server-side; the browser never writes to the database.
- Only the exact export format (
format: "studyforge-course") is accepted. - Every field is run through Zod at the server boundary:
- YouTube video IDs must match the 11-character video ID format.
- Thumbnail URLs must point to the ytimg.com CDN.
- Text fields are trimmed and length-capped (titles, descriptions, goals, module names).
- Counts are capped (50 modules, 200 lessons per module) and total file size is capped at 5 MB.
- Imported content stores YouTube metadata and IDs only — never URLs to arbitrary hosts beyond the validated thumbnails, and never video content.
- Import always creates a new course; it cannot overwrite or merge into existing data. A server-side check rejects any import whose title and ordered module names match an existing course (case-insensitive) before any write.
AI assistance (course creation, module creation, and missing-module suggestions) is a V1 feature with the following security posture:
- The provider key (
OPENROUTER_API_KEY) is read server-side only and is never exposed to the browser; the browser only calls server actions. - All provider calls are HTTPS requests with a bounded timeout; provider failures are surfaced as plain user-facing errors and never corrupt local data.
- Prompts are built only from curriculum metadata (titles, descriptions, goals, bounded lesson-title samples) — never from arbitrary user files or the raw database.
- Every AI response is treated as untrusted input and run through Zod at the server boundary before it can be saved: outlines (titles, descriptions, topics), search queries, and video selections.
- Video IDs selected by the AI are validated against the 11-character YouTube ID format before any lesson is created.
- AI output is never persisted directly; it is first shown to the learner for review, who can edit or reject it before "Accept" writes ordinary course/module/lesson rows.
StudyTube should expose as little as necessary, trust as little as necessary, validate every external input, and keep learner data and server credentials behind clear boundaries.
The fundamental security model is:
UNTRUSTED
│
▼
┌───────────┐
│ Browser │
└─────┬─────┘
│
Validate input
│
▼
┌───────────┐
│ Server │
└─────┬─────┘
│ │
▼ ▼
SQLite YouTube
│
▼
Learner Data
Security must remain part of the architecture rather than being added after implementation.