Skip to content

Conversation

@spellcascade
Copy link
Member

@spellcascade spellcascade commented Sep 1, 2025

What does this PR do?

This PR makes PatchCSPHeaders function support a kind which is either style or script.

How did you verify your code works?

Manual testing on inline scripts.

Summary by CodeRabbit

  • New Features

    • Automatic CSP handling now supports both inline scripts and styles, generating nonces when required to enable safe, standards-compliant injection.
  • Refactor

    • Consolidated CSP logic to a single mechanism for more reliable header patching across scenarios.
  • Tests

    • Added comprehensive tests covering nonce placement and behavior for script and style directives.

Copy link
Member

@anfragment anfragment left a comment

Choose a reason for hiding this comment

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

Thoughts on moving this into a separate package?

@spellcascade
Copy link
Member Author

spellcascade commented Sep 3, 2025

@anfragment Good call. I was thinking about this but hadn't come up with a proper name/location. Do you think a top-level csp package makes sense?

Copy link
Member

@anfragment anfragment left a comment

Choose a reason for hiding this comment

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

Overall looks good to me, mostly minor comments.

@anfragment anfragment self-assigned this Sep 3, 2025
@anfragment anfragment added in progress Actively under development and removed in progress Actively under development labels Sep 3, 2025
@anfragment
Copy link
Member

LGTM, just wondering what coderabbit has to say
@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

🧩 Analysis chain

I'll review the PR changes for you. Let me examine the code to provide a thorough analysis.


🏁 Script executed:

#!/bin/bash
# Get the PR diff to understand what changes were made
gh pr diff 439 --repo ZenPrivacy/zen-desktop

# Also get the list of files changed
gh pr view 439 --repo ZenPrivacy/zen-desktop --json files --jq '.files[].path'

Length of output: 16559


@anfragment I'll provide a thorough review of this CSP patching enhancement.

Code Review

This is an excellent refactoring that addresses both architectural concerns and functional requirements. Here are my findings:

Strengths

  1. Proper Package Organization: Moving CSP logic from internal/scriptlet to internal/csp creates better separation of concerns and follows Go's package organization best practices.

  2. Extensible Design: The introduction of inlineKind parameter makes the function generic enough to handle both scripts and styles, which directly addresses the PR objectives.

  3. Comprehensive Test Coverage: The new internal/csp/patch_test.go includes thorough test cases covering:

    • Both InlineScript and InlineStyle scenarios
    • Directive priority handling
    • Edge cases like missing CSP headers
    • Multiple CSP policies
  4. Security Best Practices:

    • Uses crypto/rand for nonce generation
    • Properly handles 'strict-dynamic' directive (only applies to scripts, not styles)
    • Maintains CSP directive priority correctly
  5. Clean API: The exported PatchHeaders function has a clear, self-documenting signature.

🔍 Technical Observations

  1. CSP Directive Priority: The implementation correctly handles the specificity hierarchy:

    • For scripts: script-src-elem > script-src > default-src
    • For styles: style-src-elem > style-src > default-src
  2. Multiple Policy Handling: Correctly processes multiple CSP headers independently, ensuring the most restrictive policy is respected.

  3. Backward Compatibility: The changes maintain full backward compatibility while extending functionality.

💡 Minor Suggestions

  1. Documentation: Consider adding package-level documentation for the csp package explaining its purpose and usage patterns.

  2. Error Handling: The function currently returns an empty nonce when no patching is needed. Consider documenting this behavior more explicitly in the function comment.

📋 Verification

The refactoring successfully:

  • ✅ Moves CSP logic to dedicated package as discussed
  • ✅ Supports both inline scripts and styles via kind parameter
  • ✅ Maintains all existing functionality
  • ✅ Includes comprehensive test coverage
  • ✅ Updates all references correctly

This PR represents a solid improvement to the codebase architecture while extending functionality. The code is well-tested, secure, and follows Go best practices.


🏁 Script executed:

#!/bin/bash
# Let me also check if there are any other references to the old csp.go file that might need updating
rg -n "scriptlet.*csp" --type go

# Check for any imports of the old package structure
rg -n "internal/scriptlet.*csp" --type go

# Look for any TODO comments or similar in the new csp package
rg -n "TODO|FIXME|XXX" internal/csp/

Length of output: 117


Perfect! The additional verification confirms that the refactoring is complete and clean:

  • ✅ No remaining references to the old scriptlet.*csp pattern
  • ✅ No lingering imports pointing to the old package structure
  • ✅ No outstanding TODO/FIXME comments in the new CSP package

This validates that the migration was done thoroughly with proper attention to detail. The PR is ready and represents a high-quality improvement to the codebase architecture.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

Walkthrough

Generalizes CSP patching from scripts to both scripts and styles by introducing an inlineKind API and exported PatchHeaders. Updates injector to use csp.PatchHeaders for script injection. Adds comprehensive CSP patch tests; trims injector tests to focus on script presence, removing nonce-specific assertions.

Changes

Cohort / File(s) Summary
CSP patching API generalization
internal/csp/patch.go, internal/csp/patch_test.go
Renames package to csp; replaces patchCSPHeaders with exported PatchHeaders(h, kind). Adds inlineKind with InlineScript/InlineStyle. Generalizes directive priority and allowsInline per kind. Updates patching across enforced/report-only headers. Adds unit tests covering script/style nonce placement and no-op cases.
Injector integration
internal/scriptlet/injector.go
Switches to csp.PatchHeaders(res.Header, csp.InlineScript) for nonce/CSP handling; adjusts import. Core injection flow unchanged.
Injector tests refocus
internal/scriptlet/injector_test.go
Removes nonce-specific tests and helpers. Updates hasScriptTag to parse HTML from io.ReadCloser and only verify scriptlet injection presence. Cleans imports.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Injector
  participant CSP as csp.PatchHeaders
  Client->>Injector: HTTP Response (headers, body)
  activate Injector
  Injector->>CSP: PatchHeaders(headers, InlineScript)
  activate CSP
  CSP-->>Injector: nonce (maybe empty), patched headers
  deactivate CSP
  alt nonce returned
    Injector->>Injector: Inject <script nonce="..."> into <head>
  else no nonce
    Injector->>Injector: Inject <script> or skip nonce
  end
  Injector-->>Client: Modified response
  deactivate Injector
  note over CSP: Also patches Report-Only if present
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I tap my paws on headers tight,
A nonce appears—now scripts take flight.
For styles, too, I stitch the seam,
Two kinds, one hop, a tidy scheme.
In burrows deep, I patch with cheer,
Hop-safe CSPs—all clear! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch spellcascade/csp-for-styles

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@anfragment anfragment merged commit 4b68590 into master Sep 5, 2025
16 of 18 checks passed
@anfragment anfragment deleted the spellcascade/csp-for-styles branch September 5, 2025 15:00
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/csp/patch.go (1)

117-120: Handle crypto/rand failures; ignoring the error can yield predictable nonces

rand.Read’s error is ignored. On failure/short read, nonce quality is undefined. Fail closed (panic) or propagate an error.

Apply this diff:

 func newCSPNonce() string {
@@
-	var b [18]byte // 144 bits
-	rand.Read(b[:])
+	var b [18]byte // 144 bits
+	if _, err := rand.Read(b[:]); err != nil {
+		// Extremely unlikely; fail closed rather than emitting an invalid/empty nonce.
+		panic("csp: crypto/rand failed: " + err.Error())
+	}
 	return base64.StdEncoding.EncodeToString(b[:])
 }
internal/csp/patch_test.go (1)

1-191: Replace non-existent strings.SplitSeq usage
internal/csp/patch_test.go:176: change

rawDirs := strings.SplitSeq(line, ";")

to

rawDirs := strings.Split(line, ";")
♻️ Duplicate comments (2)
internal/csp/patch_test.go (2)

157-167: Avoid false positives when asserting nonce placement; parse directives

Use the same parser (dirHasNonce) instead of strings.Contains on the whole line.

Apply this diff:

-		token := "'nonce-" + nonce + "'"
-		found := false
-		for _, line := range h.Values("Content-Security-Policy") {
-			if strings.Contains(strings.ToLower(line), tc.wantDirective) && strings.Contains(line, token) {
-				found = true
-				break
-			}
-		}
-		if !found {
+		if !dirHasNonce(h, tc.wantDirective, nonce) {
 			t.Errorf("%s: nonce not placed in %s; header: %s", tc.name, tc.wantDirective, strings.Join(h.Values("Content-Security-Policy"), " | "))
 		}

1-8: Optional: add package-level doc for csp package and PatchHeaders behavior

A brief package doc and note on returning empty nonce when no patch needed will help readers.

🧹 Nitpick comments (3)
internal/csp/patch.go (3)

81-85: Treat 'none' case-insensitively

Real-world CSP often varies casing; use a case-insensitive check.

Apply this diff:

-		if bestValue == "'none'" {
+		if strings.EqualFold(strings.TrimSpace(bestValue), "'none'") {
 			newValue = nonceToken
 		} else {
 			newValue = bestValue + " " + nonceToken
 		}

126-149: Make keyword matching case-insensitive in allowsInline

Normalize tokens before comparing CSP keywords to avoid casing pitfalls.

Apply this diff:

-	for _, t := range tokens {
-		switch t {
-		case "'unsafe-inline'":
+	for _, t := range tokens {
+		tl := strings.ToLower(t)
+		switch tl {
+		case "'unsafe-inline'":
 			unsafeInline = true
-		case "'strict-dynamic'":
+		case "'strict-dynamic'":
 			if kind == InlineScript {
 				return false
 			}
 		default:
 			if isNonceOrHashSource(t) {
 				return false
 			}
 		}
 	}

25-39: Micro-opt: avoid generating a nonce if nothing will be patched

You can pre-scan directives (using existing helpers) to decide if patching is needed before allocating a nonce. Not critical, but trims work on permissive CSPs.

Also applies to: 42-100

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d18e85e and 246b437.

📒 Files selected for processing (4)
  • internal/csp/patch.go (8 hunks)
  • internal/csp/patch_test.go (1 hunks)
  • internal/scriptlet/injector.go (2 hunks)
  • internal/scriptlet/injector_test.go (0 hunks)
💤 Files with no reviewable changes (1)
  • internal/scriptlet/injector_test.go
🔇 Additional comments (3)
internal/scriptlet/injector.go (1)

11-11: LGTM: centralized CSP patching usage

Importing internal/csp and calling csp.PatchHeaders(res.Header, csp.InlineScript) is correct; nonce handling is sound.

Also applies to: 69-76

internal/csp/patch_test.go (2)

10-45: Nice coverage on “no CSP header” and “‘none’ replacement”

Good behavioral checks and strict equality on the single-header case.


47-103: Solid priority tests for script/style directives

The directive precedence logic is well exercised for both kinds.

Also applies to: 105-139

Comment on lines +176 to +185
rawDirs := strings.SplitSeq(line, ";")

for raw := range rawDirs {
d := strings.TrimSpace(raw)
if d == "" {
continue
}
name, value := cutDirective(d)
if name == dir && strings.Contains(value, token) {
return true
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Tests don’t compile: use strings.Split and iterate values, not indexes

strings.SplitSeq doesn’t exist; and for raw := range rawDirs iterates indexes (and won’t type-check with TrimSpace).

Apply this diff:

-		rawDirs := strings.SplitSeq(line, ";")
-
-		for raw := range rawDirs {
+		rawDirs := strings.Split(line, ";")
+
+		for _, raw := range rawDirs {
 			d := strings.TrimSpace(raw)
 			if d == "" {
 				continue
 			}
 			name, value := cutDirective(d)
 			if name == dir && strings.Contains(value, token) {
 				return true
 			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rawDirs := strings.SplitSeq(line, ";")
for raw := range rawDirs {
d := strings.TrimSpace(raw)
if d == "" {
continue
}
name, value := cutDirective(d)
if name == dir && strings.Contains(value, token) {
return true
rawDirs := strings.Split(line, ";")
for _, raw := range rawDirs {
d := strings.TrimSpace(raw)
if d == "" {
continue
}
name, value := cutDirective(d)
if name == dir && strings.Contains(value, token) {
return true
}
}
🤖 Prompt for AI Agents
In internal/csp/patch_test.go around lines 176 to 185, the test fails to compile
because it uses a non-existent strings.SplitSeq and iterates indexes with "for
raw := range rawDirs" (which yields ints), causing a type mismatch with
strings.TrimSpace; replace strings.SplitSeq(line, ";") with strings.Split(line,
";") and iterate values using "for _, raw := range rawDirs" so raw is a string
that can be TrimSpace'd, keeping the subsequent logic unchanged.

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.

3 participants