Skip to content

Conversation

@shsteimer
Copy link
Owner

Overview

Adds built-in XSS protection with attribute sanitization, URL scheme validation, and same-origin enforcement for template includes.

What's New

  • 🔒 Default XSS Protection: Blocks dangerous attributes (on*, srcdoc), validates URL schemes, enforces same-origin
  • 📦 Dual Bundle: faintly.js (2736b) + faintly.security.js (646b) = 3382 bytes gzipped (under 6KB limit)
  • 🔧 Configurable: Disable with security: false, customize with config, or provide custom hooks
  • 🛡️ Template Security: Initial template fetch now protected by same-origin check
  • 🚀 Performance: Security initialized once per render, no overhead in directives

Security Features

Attribute Sanitization

  • Blocks event handlers: /^on/i pattern (onclick, onerror, etc.)
  • Blocks dangerous attributes: srcdoc
  • Validates URL schemes in href, src, action, formaction, xlink:href
  • Allows: http:, https:, mailto:, tel: (relative URLs always allowed)

Template Include Protection

  • Enforces same-origin for all template includes
  • Protects initial template fetch in resolveTemplate()
  • Blocks cross-origin URLs early with clear warnings

Documentation

  • ✅ Comprehensive README security section with examples
  • ✅ AGENTS.md updated with security module guidelines
  • ✅ Strong warnings about user-supplied data in context

Testing

  • 94 tests passed, 0 skipped
  • 100% code coverage maintained
  • ✅ All existing tests pass (backward compatible)
  • ✅ TDD approach for all security features

Architecture

  • Security initialization in renderElement() (single point, happens once)
  • initializeSecurity() exported for tests calling directives directly
  • Directives use context.security directly (clean separation)
  • Security module dynamically loaded on first use (tree-shakeable)

Backward Compatibility

Fully backward compatible - existing code works unchanged

  • Security enabled by default (transparent to users)
  • security: false or security: 'unsafe' to disable
  • Custom security hooks supported

Breaking Changes

None. This is a minor version bump (1.0.11.1.0).

Bundle Size

  • Core: 2736 bytes gzipped (under 4KB limit)
  • Security: 646 bytes gzipped
  • Total: 3382 bytes gzipped (well under 6KB limit)

Commits

  1. Phase 1: Clean slate with directives.js security integration
  2. Implement shouldAllowAttribute with TDD approach
  3. Remove inline comments from DEFAULT_CONFIG
  4. Unskip default security test for processAttributes
  5. Refactor to allowedTemplatePaths and unskip all tests
  6. Simplify allowIncludePath to same-origin only
  7. Add comprehensive security documentation to README
  8. Update AGENTS.md with security module documentation
  9. Refactor security initialization to renderElement
  10. Bump version to 1.1.0 for security feature release

…/test-runner -> 0.20.2\n- @babel/eslint-parser -> 7.28.5\n- eslint-plugin-import -> 2.32.0\n- esbuild -> 0.25.11\n- overrides: koa@2.16.3, @babel/helpers@^7.28.4\n- refresh lockfile\n\nAll tests pass; build stays under size limit.,
… and node env for scripts; size: add core+total gzip caps
- Simplified size check execution in `build.mjs` to directly exit with the appropriate code.
- Updated `check-size.mjs` to use a consistent return code structure and improved logging for size limits.
- Removed redundant function calls and streamlined the size check process for better clarity and maintainability.
- Keep all directives.js changes with getSecurity() integration
- Revert all other files to main branch baseline
- Create minimal security stub (allows everything by default)
- Add comprehensive tests for security integration in directives
  - Test custom security hooks
  - Test unsafe mode (security: false and 'unsafe')
  - Add placeholder skipped tests for default security validation
- Delete complex security test files for incremental rebuild
- All tests pass: 72 passed, 2 skipped
- 100% code coverage maintained
- Bundle size: 2680 bytes (under 4096 limit)
- Add comprehensive test suite for security module (15 tests)
- Implement attribute blocking by pattern (event handlers) and name (srcdoc)
- Implement URL scheme validation with URL constructor
- Refactor helper functions to top level with JSDoc
- Relative URLs now explicitly use window.location.protocol
- All tests pass: 88 passed, 2 skipped
- 100% code coverage maintained
- Bundle: 3361 bytes gzipped (under 6144 limit)
- Clean up DEFAULT_CONFIG object (comments weren't adding value)
- Property names are self-explanatory
- Reduces bundle size by 61 bytes (3361 -> 3300 bytes)
- All tests still pass
- Test now verifies default security module loads and blocks dangerous attributes
- Validates that onclick is blocked while safe attributes like class are allowed
- 89 tests passing, 1 skipped (down from 2 skipped)
- 100% coverage maintained
- Replace includeBasePath with allowedTemplatePaths array
- Remove variable substitution complexity
- Default to context.codeBasePath (or / as fallback)
- Users can provide custom array for more control
- Unskip and implement last processInclude test
- Add test for blocking paths outside codeBasePath
- All tests pass: 98 passed, 0 skipped
- 100% coverage maintained
- Bundle: 3456 bytes gzipped (under 6144 limit)
- Remove allowedTemplatePaths config entirely
- Default security enforces same-origin for includes (critical boundary)
- Users can provide custom allowIncludePath for additional restrictions
- Simpler implementation and API
- All tests pass: 95 passed, 0 skipped
- 100% coverage maintained
- Bundle: 3326 bytes gzipped (130 bytes smaller!)
- Document default security features (attribute sanitization, URL validation, same-origin)
- Show custom security hooks and configuration options
- Move unsafe mode section to bottom with stronger warnings
- Add explicit WARNING about user-supplied data in context
- Clarify trust boundaries (what is protected vs trusted by design)
- Update Getting Started to mention faintly.security.js file
- Add security to rendering context list
- Add security module to project purpose
- Document dual bundle architecture (core + security)
- Update build size limits (4KB core, 6KB total)
- Add security module section with development guidelines
- Update repo layout to include test/security/
- Clarify that security is enabled by default
- Emphasize testing and conservative defaults for security changes
- Move security initialization to renderElement() - happens once per render
- Add security check to resolveTemplate() - protects initial template fetch
- Export initializeSecurity() for tests that call directives directly
- Remove getSecurity() helper - no longer needed
- Directives now use context.security directly (always initialized)
- Clean separation: renderElement handles init, directives use it
- Update all tests to initialize security when calling directives directly
- Add test for template security blocking
- All tests pass: 94 passed, 0 skipped
- 100% coverage maintained
- Bundle: 3430 bytes gzipped
const { updated, updatedText } = await resolveExpressions(el.getAttribute(attrName), context);
if (updated) el.setAttribute(attrName, updatedText);
if (updated) {
if (!context.security.shouldAllowAttribute(attrName, updatedText, context)) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor, but can this be a positive check same as in processAttributesDirective

const allowed = context.security.allowIncludePath(templatePath, context);
if (!allowed) {
// eslint-disable-next-line no-console
console.warn(`Blocked include outside allowed scope: ${new URL(templatePath, window.location.origin).href}`);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice a console warn here, lets be consistent and either always warn on security violations or never

- Change negative check to positive check in processAttributes for consistency
- Remove console.warn calls for silent security violations
- Security failures now consistently silent (attributes removed, includes blocked)
- Still throws error for template fetch blocking (prevents app from continuing)
@shsteimer shsteimer merged commit bb0c5e4 into main Oct 29, 2025
1 check passed
@shsteimer shsteimer deleted the feat/security-hardening branch October 29, 2025 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants