Skip to content

Fix CSRF conflicts with Phoenix using automatic pipeline separation#51

Open
camilohollanda wants to merge 4 commits intotompave:masterfrom
prem-prakash:configurable-csrf-protection
Open

Fix CSRF conflicts with Phoenix using automatic pipeline separation#51
camilohollanda wants to merge 4 commits intotompave:masterfrom
prem-prakash:configurable-csrf-protection

Conversation

@camilohollanda
Copy link

@camilohollanda camilohollanda commented Feb 10, 2026

Summary

This PR implements automatic pipeline separation to resolve CSRF conflicts with Phoenix applications without requiring any configuration. Static assets are served without CSRF protection while interactive routes maintain full security.

Fixes #52

Problem

When embedding FunWithFlags UI in Phoenix applications, conflicts occur because static assets (JavaScript/CSS) are served through CSRF-protected routes. This results in cross-origin request errors like:

** (Plug.CSRFProtection.InvalidCrossOriginRequestError) security warning: an embedded <script> tag on another site requested protected JavaScript (if you know what you're doing, disable forgery protection for this route)

The issue occurs because:

  1. FunWithFlags UI serves static assets via Plug.Static at /assets
  2. All requests go through the same CSRF-protected pipeline
  3. Phoenix's CSRF protection conflicts with this, treating asset requests as potentially dangerous cross-origin requests
  4. JavaScript and CSS assets fail to load, breaking the UI

Current State: README Workaround vs. User Experience

README Documents a Solution

The current README provides a workaround using :mounted_apps pipeline:

pipeline :mounted_apps do
  plug :accepts, ["html"]
  plug :put_secure_browser_headers
  # No :protect_from_forgery - relies on FunWithFlags' own CSRF
end

scope path: "/feature-flags" do
  pipe_through :mounted_apps
  forward "/", FunWithFlags.UI.Router
end

Why This Creates Poor User Experience

1. Discoverability Problem

  • New users naturally try the standard Phoenix approach first
  • Hit CSRF errors with no clear path to the solution
  • Must research and discover the special pipeline requirement

2. Non-intuitive Integration
Most Phoenix developers expect this to work:

scope "/admin" do
  pipe_through [:browser, :require_authenticated_user]  # Standard approach
  forward "/feature-flags", FunWithFlags.UI.Router
end

3. Maintenance Overhead

  • Requires creating and maintaining special pipelines
  • Adds router complexity
  • Phoenix updates might affect custom pipeline configuration

4. Documentation Burden

  • Users must understand CSRF conflicts to know they need the workaround
  • Not clear when :browser vs :mounted_apps should be used

Solution: Automatic Pipeline Separation

Implemented automatic pipeline separation that routes requests based on path:

  • /assets/* → Asset pipeline (static file serving only, no CSRF)
  • All other routes → Form pipeline (full CSRF protection + token assignment)

Architecture

defp route_by_type(conn, _opts) do
  case conn.request_path do
    "/assets" <> _ -> asset_pipeline(conn)    # No CSRF
    _ -> form_pipeline(conn)                  # Full CSRF
  end
end

This follows the Phoenix pattern:

  1. HTML pages get CSRF tokens embedded for JavaScript
  2. Static assets load without CSRF interference
  3. JavaScript reads tokens from HTML and uses them for AJAX requests

Benefits Over Current Approach

Aspect Current (:mounted_apps) This PR (Pipeline Separation)
Configuration ❌ Requires special pipeline setup ✅ Zero configuration
Discoverability ❌ Must read docs to find workaround ✅ Works with standard :browser pipeline
Maintenance ❌ Extra pipeline to maintain ✅ No additional setup
User Experience ❌ Confusing for new users ✅ "Just works" approach
Security ✅ Maintains CSRF protection ✅ Better separation (assets vs forms)

Usage

No changes needed - works automatically with standard Phoenix patterns:

# This now works without any special configuration
scope "/admin" do
  pipe_through [:browser, :require_authenticated_user, :require_admin]
  forward "/feature-flags", FunWithFlags.UI.Router
end

# Still works with namespace
forward "/admin", FunWithFlags.UI.Router, namespace: "feature-flags"

# And still compatible with existing :mounted_apps approach
scope path: "/feature-flags" do
  pipe_through :mounted_apps
  forward "/", FunWithFlags.UI.Router
end

Testing

The implementation maintains complete backward compatibility while making the intuitive approach work automatically. This eliminates the need for users to discover and implement workarounds for CSRF conflicts.

…cations

This change introduces a disable_csrf option that allows host applications
to disable FunWithFlags UI's built-in CSRF protection when they provide
their own CSRF protection (like Phoenix applications do).

Changes:
- Add disable_csrf option to router configuration
- Make protect_from_forgery plug conditional based on disable_csrf setting
- Make CSRF token assignment conditional
- Update module documentation with usage examples

This resolves conflicts that occur when embedding FunWithFlags UI in
Phoenix applications that already have CSRF protection enabled.

Usage:
forward "/feature-flags", FunWithFlags.UI.Router, disable_csrf: true
Clarify that namespace is used for internal redirects and provide a concrete
example showing when and how to use it when the forward path differs from
the desired UI path structure.
This commit replaces the disable_csrf configuration option with an
automatic pipeline separation approach that resolves CSRF conflicts
with Phoenix applications while maintaining security.

Changes:
- Remove disable_csrf option and related conditional logic
- Implement automatic request routing by path:
  * /assets/* → Asset pipeline (no CSRF protection)
  * All other routes → Form pipeline (full CSRF protection)
- Maintain CSRF tokens in HTML pages for JavaScript AJAX requests
- Update documentation to explain the automatic approach

Benefits:
- No configuration required - works automatically
- Fixes InvalidCrossOriginRequestError for JavaScript/CSS assets
- Maintains security for all form submissions and API calls
- Follows Phoenix pattern of separating assets from interactive routes

This resolves conflicts when embedding in Phoenix applications while
providing better security than fully disabling CSRF protection.
@camilohollanda camilohollanda changed the title Add configurable CSRF protection to prevent conflicts with host applications Fix CSRF conflicts with Phoenix using automatic pipeline separation Feb 10, 2026
This commit adds extensive test coverage for the new pipeline separation
feature that resolves CSRF conflicts with Phoenix applications.

Test coverage includes:

**Pipeline Separation Tests:**
- Asset requests (/assets/*) go through asset pipeline without CSRF tokens
- Different asset extensions (js, css, png, woff) all use asset pipeline
- Form requests go through form pipeline with CSRF tokens assigned
- All form routes receive CSRF tokens for JavaScript usage
- POST requests work correctly with form pipeline and CSRF protection
- Asset pipeline doesn't interfere with static file serving

**CSRF Token Assignment Tests:**
- HTML responses include CSRF tokens for JavaScript AJAX requests
- Asset requests never receive CSRF tokens to avoid conflicts
- Token assignment is consistent across different routes

**Backward Compatibility Tests:**
- All existing functionality continues to work unchanged
- Namespace option works correctly with pipeline separation
- Flag operations (create, read, update) work with new architecture

The tests ensure the pipeline separation provides the expected security
benefits (CSRF protection for forms, no conflicts for assets) while
maintaining complete backward compatibility with existing usage.

Tests require Redis for FunWithFlags functionality and will run in CI.
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.

CSRF conflicts when embedding in Phoenix applications cause assets to fail loading

1 participant