Skip to content

Conversation

@kasyap1234
Copy link
Contributor

What does this PR do?

This PR adds support for patching Content-Security-Policy (CSP) directives defined in HTML <meta http-equiv="Content-Security-Policy"> tags, in addition to those set via HTTP headers.

Previously, csp.PatchHeaders only handled CSPs in response headers, which caused injection failures on websites that define their CSPs using meta tags (e.g., LinkedIn).

To fix this, the PatchHeaders function now accepts a *http.Response instead of http.Header, enabling unified handling of both header-based and meta-based CSPs.

BREAKING CHANGE:
PatchHeaders(h http.Header, kind inlineKind) (nonce string)
PatchHeaders(res *http.Response, kind inlineKind) (string, error)


How did you verify your code works?

  • Added tests for HTML responses containing <meta http-equiv="Content-Security-Policy"> tags.
  • Verified CSP nonces are correctly injected into both header and meta tag policies.
  • Manually tested on pages relying solely on meta tag CSPs to confirm inline script/style functionality.

What are the relevant issues?

Closes #442

@coderabbitai
Copy link

coderabbitai bot commented Oct 10, 2025

Walkthrough

Refactors CSP patching to operate on the full *http.Response, adds streaming rewrite support for , returns (nonce, error), surfaces patch errors, and updates injector callsites and tests to use the new API and error handling. No exported type names changed; PatchHeaders signature changed.

Changes

Cohort / File(s) Summary
CSP patcher core
internal/csp/patch.go
Refactors PatchHeaders to accept *http.Response and return (string, error); adds streaming meta-tag CSP rewriting via patchMetaCSPs; introduces patchPolicies and patchOneHeader helpers; centralizes nonce generation; returns errors when patching fails; returns empty nonce when no patches applied.
CSP tests
internal/csp/patch_test.go
Updates tests to construct and pass *http.Response (headers + body) to PatchHeaders, handle its (nonce, error) return, and add/adjust cases covering header-only, meta-only, and combined CSP scenarios.
Injectors
internal/cosmetic/injector.go, internal/cssrule/injector.go, internal/extendedcss/injector.go, internal/scriptlet/injector.go
Replace csp.PatchHeaders(res.Header, ...) with csp.PatchHeaders(res, ...); capture returned (nonce, err) and return wrapped errors on failure; reuse existing err variable when executing templates and pass nonce into template data.

Sequence Diagram(s)

sequenceDiagram
  participant Injector
  participant CSP as csp.PatchHeaders
  participant Res as http.Response
  participant Meta as meta tags
  participant Headers as response headers

  Injector->>CSP: PatchHeaders(Res, kind)
  note right of CSP: generate nonce (if needed)
  CSP->>Meta: scan & stream-rewrite <meta http-equiv="Content-Security-Policy">
  alt meta patched
    Meta-->>CSP: meta changed
  else meta unchanged
    Meta-->>CSP: no-op
  end
  CSP->>Headers: rewrite CSP header values via patchPolicies
  alt any patch applied
    Headers-->>CSP: headers changed
    CSP-->>Injector: (nonce, nil)
    Injector->>Injector: execute templates using nonce
  else no patches
    CSP-->>Injector: ("", nil)
    Injector->>Injector: continue without nonce
  end
  opt error during patch
    CSP-->>Injector: ("", error)
    Injector-->>Injector: return wrapped error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to focus review on:

  • internal/csp/patch.go — correctness of streaming HTML rewrite (tokenization boundaries, early termination at head/body), concurrency/stream-safety, and proper header/body writes.
  • Error propagation and wrapping consistency where PatchHeaders is now called from multiple injectors.
  • Tests in internal/csp/patch_test.go — ensure coverage for meta-only, header-only, and combined cases and that response body/head positions are modeled correctly.

Possibly related PRs

Suggested reviewers

  • spellcascade
  • anfragment

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "feat(csp): patch CSP meta tags and update PatchHeaders signature" clearly and concisely summarizes the main changes in the changeset. It identifies the two primary modifications: adding support for patching CSP meta tags and updating the PatchHeaders function signature. The title is specific enough that a teammate reviewing commit history would immediately understand the primary purpose of these changes. The phrasing is direct and avoids generic terms or unnecessary noise.
Linked Issues Check ✅ Passed The code changes successfully implement all acceptance criteria from issue #442. The implementation adds patchMetaCSPs using httprewrite.StreamRewrite to identify and patch CSPs in tags, matching the specified implementation approach. Tests were added covering meta tag handling and header/meta interplay. The PatchHeaders function was updated to accept *http.Response, enabling unified handling of both header-based and meta-based CSPs. All five injector files were properly updated to handle the new signature and error return value. Existing header patching logic is preserved and functional, with no apparent regressions introduced.
Out of Scope Changes Check ✅ Passed All changes in the pull request are directly related to implementing the feature and handling the necessary updates from the breaking signature change. The core implementation in internal/csp/patch.go adds the patchMetaCSPs function and updates PatchHeaders as required. Updates to internal/cosmetic/injector.go, internal/cssrule/injector.go, internal/extendedcss/injector.go, and internal/scriptlet/injector.go are necessary adaptations to the PatchHeaders signature change, properly adding error handling throughout. The test file updates directly reflect the new function signature and add coverage for meta tag scenarios. No extraneous changes or unrelated modifications are present.
Description Check ✅ Passed The pull request description comprehensively addresses all required sections from the template. The "What does this PR do?" section clearly explains the feature addition, the problem it solves (injection failures on sites using meta tag CSPs like LinkedIn), and includes the breaking change details with before/after signatures. The "How did you verify your code works?" section describes both automated testing (tests for HTML meta tag responses) and manual verification on pages relying solely on meta tag CSPs. The "What are the relevant issues?" section properly links issue #442. The description is complete and provides sufficient context for understanding the scope and motivation of the changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8eca3bb and cc73238.

📒 Files selected for processing (1)
  • internal/csp/patch.go (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-05T11:40:45.127Z
Learnt from: spellcascade
Repo: ZenPrivacy/zen-desktop PR: 436
File: internal/app/app.go:191-202
Timestamp: 2025-09-05T11:40:45.127Z
Learning: In the ZenPrivacy/zen-desktop codebase, the user spellcascade prefers simpler approaches over complex error handling mechanisms when the added complexity outweighs the benefit, particularly for resource cleanup scenarios in internal/app/app.go.

Applied to files:

  • internal/csp/patch.go
📚 Learning: 2025-09-08T15:48:08.326Z
Learnt from: anfragment
Repo: ZenPrivacy/zen-desktop PR: 0
File: :0-0
Timestamp: 2025-09-08T15:48:08.326Z
Learning: In the zen-desktop project, content injection guards should be implemented at the centralized filtering point in internal/filter/filter.go HandleResponse method, rather than in individual injectors, since there are multiple injectors (scriptlets, cosmetic rules, CSS rules, JS rules) that all process responses.

Applied to files:

  • internal/csp/patch.go
🧬 Code graph analysis (1)
internal/csp/patch.go (1)
internal/httprewrite/rawbody.go (1)
  • StreamRewrite (27-46)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🧹 Nitpick comments (1)
internal/extendedcss/injector.go (1)

97-100: Preserve error wrapping with %w.

We’re standardizing on %w so callers can unwrap the root cause. Please change fmt.Errorf("patch CSP headers: %v", err) to use %w, matching the other injectors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25af984 and 8eca3bb.

📒 Files selected for processing (6)
  • internal/cosmetic/injector.go (1 hunks)
  • internal/csp/patch.go (4 hunks)
  • internal/csp/patch_test.go (5 hunks)
  • internal/cssrule/injector.go (1 hunks)
  • internal/extendedcss/injector.go (1 hunks)
  • internal/scriptlet/injector.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
internal/csp/patch.go (1)
internal/httprewrite/rawbody.go (1)
  • StreamRewrite (27-46)
internal/extendedcss/injector.go (1)
internal/csp/patch.go (2)
  • PatchHeaders (31-51)
  • InlineScript (24-24)
internal/cosmetic/injector.go (1)
internal/csp/patch.go (2)
  • PatchHeaders (31-51)
  • InlineStyle (25-25)
internal/csp/patch_test.go (1)
internal/csp/patch.go (3)
  • PatchHeaders (31-51)
  • InlineScript (24-24)
  • InlineStyle (25-25)
internal/scriptlet/injector.go (1)
internal/csp/patch.go (2)
  • PatchHeaders (31-51)
  • InlineScript (24-24)
internal/cssrule/injector.go (1)
internal/csp/patch.go (2)
  • PatchHeaders (31-51)
  • InlineStyle (25-25)

@anfragment anfragment self-assigned this Oct 15, 2025
@anfragment anfragment added the review Completed and ready for evaluation label Oct 15, 2025
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.

Hello @kasyap1234, sorry for the late reply.

LGTM overall, just a few minor comments below.

Comment on lines 51 to 53
// In case of multiple lines/policies, the browsers will select the most restrictive one.
// For this reason, we modify each independently so they all allow the inline tag.
// See more: https://content-security-policy.com/examples/multiple-csp-headers/.
Copy link
Member

Choose a reason for hiding this comment

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

Can we preserve this, and the other comments within the function?

var newValue string
if bestValue == "'none'" {
switch bestValue {
case "", "'none'":
Copy link
Member

Choose a reason for hiding this comment

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

I believe the "" case will never be triggered:

Suggested change
case "", "'none'":
case "'none'":

Comment on lines 128 to 136
contentType := res.Header.Get("Content-Type")
if contentType == "" {
return false, nil
}

mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil || !strings.EqualFold(mediaType, "text/html") {
return false, nil
}
Copy link
Member

Choose a reason for hiding this comment

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

I believe this is somewhat redundant in practice due to this check a couple levels above:
https://github.com/ZenPrivacy/zen-desktop/blob/master/internal/filter/filter.go#L313

defer src.Close()

z := html.NewTokenizer(src)
for {
Copy link
Member

@anfragment anfragment Oct 21, 2025

Choose a reason for hiding this comment

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

One potential performance optimization within the loop – since <meta http-equiv> can only be present in <head>, we can directly copy the remains of src to dst without the overhead of tokenization after we encounter </head> or <body>.

See an example here:
https://github.com/ZenPrivacy/zen-desktop/blob/master/internal/httprewrite/html.go#L56-L65

@anfragment
Copy link
Member

Hey @kasyap1234! Sorry for not notifying you earlier – we've transferred most of the Go packages from this repository to zen-core. Could you please manually move the code there in a new PR? The package structure and the code has remained the same, so I hope it won't take much time.

@anfragment anfragment removed the review Completed and ready for evaluation label Oct 31, 2025
@anfragment
Copy link
Member

See ZenPrivacy/zen-core#15

@anfragment anfragment closed this Nov 4, 2025
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.

Add <meta http-equiv="Content-Security-Policy"> patching to the CSP patcher

2 participants