Escape CR and LF in WARC metadata records and resource Content-Type - #2109
Conversation
MetadataRecordFormat.format() wrote one line per metadata value into the application/warc-fields payload without checking for CR or LF. A value containing CR LF (e.g. feed.description set by FeedParserBolt, or values of parse.* filters such as the XPath, LDJson and Tika filters) therefore became additional field lines that look exactly like fields written by the crawler - for example a fabricated hopsFromSeed or via. Framing stayed valid because Content-Length is computed from the finished payload, so WARC readers had no way to detect the injected fields. - replace CR and LF by spaces in metadata values written into the warc-fields payload, and drop metadata keys that are not valid WARC field names (printable ASCII without colon, RFC 5322 section 2.2) - sanitise the server-supplied Content-Type used for resource records in WARCRecordFormat.format(), which was appended verbatim into the WARC header block - log MetadataRecordFormat messages under MetadataRecordFormat instead of WARCRequestRecordFormat Fixes apache#2105
dpol1
left a comment
There was a problem hiding this comment.
Went looking for other header sinks with the same pattern in the three record formats and came up empty, so this seems to cover them all. Space over dropping the value feels right for a format with no escaping.
Small thought, non-blocking: the invalid-key warning fires per record, and keys are fixed config - would validating once in the constructor be enough?
The configured metadata keys are fixed topology configuration: check them for valid WARC field names in the constructor and drop invalid keys with a single warning, instead of repeating the check and warning for every record written. Suggested in review.
|
Thanks for the review and for double-checking the other header sinks — good to have that confirmed. Fair point on the per-record warning: the configured keys are indeed fixed topology configuration. Done in 311f3ab — |
The conjunction already returned false for a null name through short-circuit evaluation, but the intent was easy to miss. Return false explicitly and cover the field name and value sanitisation helpers with unit tests. Suggested in review.
|
👍 |
|
thanks @abhinav-phi |
Fixes #2105.
What happens
MetadataRecordFormat.format()writes one line per metadata value askey: valueinto theapplication/warc-fieldspayload of the WARC metadata record (external/warc/src/main/java/org/apache/stormcrawler/warc/MetadataRecordFormat.java, config keywarc.metadata.keys). Neither the key nor the value was checked for CR or LF, so a value containing CR LF became two or more field lines in the payload:The extra lines look exactly like fields written by the crawler. Framing stayed valid because
Content-Lengthis computed from the finished payload, so no record was split — and WARC readers had no way to tell thathopsFromSeed: 1(or a fabricatedvia, etc.) was contributed by the captured content rather than written by StormCrawler.Values that reach this sink often originate from the crawled content:
FeedParserBoltsetsfeed.descriptionfrom the feed item without trimming, andparse.*values produced by the XPath, LDJson and Tika filters keep their newlines. As soon as an operator lists such a key inwarc.metadata.keys, a crawled page or feed can add field lines to its own metadata record. The effect is limited — nothing in this repository readswarc-fieldsback, and metadata records are opt-in — but a downstream tool that ranks or filters captures by those fields can be misled.The same lack of escaping applied to the resource record
Content-TypeinWARCRecordFormat.format()(external/warc/src/main/java/org/apache/stormcrawler/warc/WARCRecordFormat.java), where the server-supplied content type was appended verbatim into the WARC header block itself — worse in principle, because it could forge arbitrary WARC header lines (WARC-Truncated,WARC-Protocol, …), even though the sibling formats happen not to feed such values into it today.The fix
WARCRecordFormatgains two shared helpers (used by its subclassMetadataRecordFormatas well):isValidWarcFieldName(String)— a WARC field name must consist of printable ASCII characters without a colon (RFC 5322 § 2.2, which the WARC specification references for named fields).MetadataRecordFormatdrops a configured key with a warning if it is not a valid field name, instead of writing a malformed line; since the keys are fixed configuration, this check runs once in the constructor rather than for every record (following review feedback).sanitizeWarcFieldValue(String)— replaces CR and LF characters by spaces so that a value cannot end its field line early and forge additional lines. Folding long values via continuation lines (as the WARC spec allows) was considered, but replacement keeps each value on a single line and avoids re-introducing interpretation questions for values that already contain line breaks.Concretely:
MetadataRecordFormat— invalid keys are dropped once in the constructor;format()sanitises every value before the field line is appended.WARCRecordFormat.format()— the server-controlledContent-Typeof resource records is sanitised before it is appended to the WARC header block.In addition,
MetadataRecordFormatlogged underWARCRequestRecordFormat.class(copy/paste); the logger now uses the correct class.Deliberately out of scope
WARC-Protocol,WARC-Truncated) are produced internally by the protocol implementations, not from captured content; touching them would also collide with the pending #1998 WARC writer: WARC-Protocol header to follow WARC field proposals #2034, which reworksWARC-Protocolwriting. (I checked #1998 WARC writer: WARC-Protocol header to follow WARC field proposals #2034 for duplication as suggested — it is about WARC-Protocol/WARC-Cipher-Suite conformance, not escaping; the only overlap is that both PRs touchWARCRecordFormat.format().)generateWARCInfo()writes operator-configured fields (WARCHdfsBolt.withHeader()), which are topology settings rather than crawled content.Tests
New
MetadataRecordFormatCRLFTest(the reproduction from the issue, extended):warc-fieldsline; the record parsed with jwarc contains only the field the crawler wrote, and the sanitised value stays on its own field line;WARCRecordFormatTest#testWarcResourceRecordContentTypeCRLFInjection: a server-suppliedContent-Typecontaining CR LF cannot forge WARC header lines; jwarc sees exactly oneContent-Typeheader and no forgedWARC-Truncated.All 17 tests of the warc module pass, together with the
editorconfig,git-code-format:validate-code-formatand-Pratchecks. (WARCHdfsBoltTestcannot run locally on Windows — pre-existingHADOOP_HOME/winutils environment limitation, unrelated to this change; it runs on Linux CI as before.)