Skip to content

Fix top-3 review findings: SSRF, textarea CRLF corruption, image fetch - #82

Closed
paskal wants to merge 1 commit into
masterfrom
fix-top3-review
Closed

Fix top-3 review findings: SSRF, textarea CRLF corruption, image fetch#82
paskal wants to merge 1 commit into
masterfrom
fix-top3-review

Conversation

@paskal

Copy link
Copy Markdown
Member

Fixes the three highest-priority issues from an end-to-end review of the current master.

1. SSRF via user-supplied URLs (security)

POST /api/extract is unauthenticated and the extractor fetched any URL it was given with no host validation, so a caller could reach http://169.254.169.254/… (cloud metadata), localhost and other internal hosts; image extraction (getImageSize) amplified it by following <img src> into more internal fetches.

Both the page retriever and the image fetcher now reject targets that resolve to loopback, private, link-local, unspecified or multicast addresses. The check runs in a net.DialerControl hook against the actual IP at connect time, so it also covers redirect-to-internal and DNS rebinding. It is gated behind a new BlockPrivateNetworks flag, wired on in main; the default stays permissive so httptest-based tests (which bind loopback) keep working.

2. Textarea CRLF corruption on rule save (data integrity)

saveRule split match_url, excludes and test_urls on "\n" only. Browsers submit textareas with \r\n, so every stored entry kept a trailing \r and blank lines became empty-string entries, and this junk accumulated on every round-trip through the edit form. New splitTrimLines trims each line and drops empties; handlePreview reuses it in place of its ad-hoc split-and-trim loop.

3. Image sizing wasted resources on every extraction (efficiency)

getImageSize buffered each image fully into memory via io.ReadAll, built a fresh http.Client per image, ignored the request context, and spawned one unbounded goroutine per <img> on the page. It now streams to io.Discard behind a 10 MB io.LimitReader, shares a single client, propagates the caller's context, and caps concurrency at 8 probes per page.

Tests

Added TestHTTPRetriever_BlockPrivateNetworks, TestIsBlockedIP, TestServer_SaveRuleTrimsLines, and two getImageSize subtests (context cancellation and streamed-byte measurement). Full suite passes with -race, golangci-lint and go vet clean.

Note

SSRF blocking is opt-in (on in production via main, off by default) to avoid rewriting the loopback-based test suite. If you'd prefer it default-on with the tests adjusted to set an allow flag, that's an easy follow-up.

Block SSRF via user-supplied URLs. Extraction and image fetches now
reject targets resolving to loopback, private, link-local or other
non-public addresses via a net.Dialer Control hook (checks the actual
IP at connect time, so it also covers redirects and DNS rebinding).
Gated by BlockPrivateNetworks, wired on in main; default stays permissive
so httptest-based tests keep working. `POST /api/extract` was and remains
unauthenticated, so this is the real mitigation.
Stop corrupting multi-line rule fields. saveRule split match_url,
excludes and test_urls on "\n" only, so browser-submitted \r\n left a
trailing \r on every entry and blank lines became empty strings. New
splitTrimLines trims and drops empties; handlePreview reuses it.
Rework image sizing. getImageSize buffered every image fully into memory
with a fresh client per image, no context and unbounded per-page
goroutines. Now it streams to io.Discard behind an io.LimitReader (10MB
cap), shares one client, honors the request context and caps concurrency
at 8 probes per page.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses three high-priority findings by hardening outbound fetching against SSRF, fixing CRLF-related textarea corruption when saving rules, and making image-size probing more resource-efficient within the extractor.

Changes:

  • Add optional private-network/loopback blocking at dial-time for page retrieval and image probing (enabled in main).
  • Normalize textarea inputs by splitting, trimming, and dropping empty lines to prevent \r/blank-entry accumulation.
  • Stream image bytes with a size cap, share a single HTTP client for image probes, propagate request context, and limit concurrent in-flight image probes.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
rest/server.goReplaces ad-hoc textarea splitting with splitTrimLines to trim CRLF and drop empty entries.
rest/server_test.goAdds coverage to ensure CRLF/blank lines are handled correctly when saving rules.
main.goEnables private-network blocking flags for production use in the HTTP retriever and image probing.
extractor/safedial.goIntroduces dial-time IP blocking helpers and a transport constructor for SSRF mitigation.
extractor/retriever.goAdds BlockPrivateNetworks to HTTPRetriever and wires safe transport when enabled.
extractor/retriever_test.goAdds tests for loopback blocking behavior and IP classification logic.
extractor/readability.goPlumbs request context into image extraction and adds image-client fields/flag.
extractor/pics.goRefactors image probing to stream bytes with a cap, reuse a client, accept context, and limit concurrent probes.
extractor/pics_test.goUpdates callers for the new context signature and adds tests for cancellation/streamed sizing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadextractor/pics.go
Comment on lines 46 to 50
wg.Go(func() {
size := f.getImageSize(im)
resCh <- imgInfo{url: im, size: size}
sem <- struct{}{}
defer func() { <-sem }()
resCh <- imgInfo{url: im, size: f.getImageSize(ctx, im)}
})

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

False positive - sync.WaitGroup.Go was added in Go 1.25 (go.mod is go 1.25.0); it compiles and passes under -race in CI. This PR is superseded by #85, where the same code carries the same reply. No change.

Comment threadextractor/pics.go
log.Printf("[WARN] can't create request to get pic from %s", url)
return 0
}
req.Close = true

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed. Dropped req.Close = true in getImageSize so the now-shared image client can keep-alive and reuse connections. Applied in the live successor PRs: #83 (164ef76) and #85 (06ffd44), both green on build/-race/lint.

Comment threadextractor/safedial.go
Comment on lines +12 to +14
// errBlockedAddress is returned when a fetch targets a non-public address and private-network
// blocking is enabled. the check runs at connect time against the actual resolved IP, so it also
// defends against DNS rebinding and redirects that point back at internal hosts.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Leaving lowercase by design - this repo's convention is lowercase in-code comments (see CLAUDE.md / the surrounding code), so a new sentence starting with lowercase "the check" is consistent house style rather than a typo. No change.

@paskal

Copy link
Copy Markdown
MemberAuthor

Superseded by three focused PRs, one logical fix each: #83 (SSRF connect-time IP guard), #84 (textarea CRLF trim), #85 (image-fetch streaming/concurrency). Each was gated with an xhigh Codex review over its final diff.

@paskalpaskal closed this Jul 3, 2026
@paskal
paskal deleted the fix-top3-review branch August 19, 2026 09:06
Sign up for freeto 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

@paskal