Module: Otto::Security::CSP::Parser

Defined in:
lib/otto/security/csp/parser.rb

Overview

Parses inbound Content-Security-Policy violation report bodies into a list of normalized Report objects.

Handles BOTH standardized wire formats:

  • Legacy application/csp-report — a single JSON object {"csp-report": { ... }}.
  • Reporting API application/reports+json — a JSON ARRAY of {"type": "csp-violation", "body": { ... }} entries (a single un-wrapped object is tolerated too).

The parser keys off the JSON SHAPE rather than trusting the declared Content-Type, because browsers and intermediaries are inconsistent about the header. The content_type argument is accepted for future use and symmetry with the middleware but is not currently required to disambiguate.

It is intentionally TOTAL: malformed JSON, an unexpected top-level type, or entries that are not CSP violations yield an empty array rather than raising. A violation-report receiver must never fail on hostile input.

Class Method Summary collapse

Class Method Details

.extract_from_object(data) ⇒ Array<Hash>

A single top-level object in either the legacy {"csp-report": {...}} envelope or a lone Reporting API {"type":..., "body": {...}} object.

Parameters:

  • data (Hash)

Returns:

  • (Array<Hash>)


105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/otto/security/csp/parser.rb', line 105

def extract_from_object(data)
  if data['csp-report'].is_a?(Hash)
    [data['csp-report']]
  elsif data['body'].is_a?(Hash) && (data['type'].nil? || data['type'] == 'csp-violation')
    # Mirror extract_from_reporting_api: accept a lone csp-violation (or
    # untyped) envelope, but skip other single-object report types
    # (deprecation, intervention, ...).
    [data['body']]
  else
    []
  end
end

.extract_from_reporting_api(entries) ⇒ Array<Hash>

Reporting API batch: an array of report envelopes. Keep entries that are (or are untyped but shaped like) CSP violations and carry a body.

Parameters:

  • entries (Array)

Returns:

  • (Array<Hash>)


84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/otto/security/csp/parser.rb', line 84

def extract_from_reporting_api(entries)
  entries.filter_map do |entry|
    next unless entry.is_a?(Hash)

    body = entry['body']
    next unless body.is_a?(Hash)

    type = entry['type']
    # Accept entries explicitly typed csp-violation, or untyped bodies.
    # Skip other report types (deprecation, intervention, ...).
    next unless type.nil? || type == 'csp-violation'

    body
  end
end

.extract_raw_reports(data, _content_type = nil) ⇒ Array<Hash>

Pull the per-violation field hashes out of either wire format.

Parameters:

  • data (Object)

    the parsed JSON structure.

  • _content_type (String, nil) (defaults to: nil)

    unused (shape drives extraction).

Returns:

  • (Array<Hash>)

    raw, un-normalized per-violation field hashes.



68
69
70
71
72
73
74
75
76
77
# File 'lib/otto/security/csp/parser.rb', line 68

def extract_raw_reports(data, _content_type = nil)
  case data
  when Array
    extract_from_reporting_api(data)
  when Hash
    extract_from_object(data)
  else
    []
  end
end

.parse(body, content_type = nil) ⇒ Array<Otto::Security::CSP::Report>

Parse a raw report body into normalized reports.

Parameters:

  • body (String, nil)

    the raw request body (JSON).

  • content_type (String, nil) (defaults to: nil)

    the request Content-Type (hint only).

Returns:

  • (Array<Otto::Security::CSP::Report>)

    zero or more normalized reports. Empty when the body is nil/blank/malformed or contains no recognizable CSP violations.



42
43
44
45
46
47
48
49
50
51
# File 'lib/otto/security/csp/parser.rb', line 42

def parse(body, content_type = nil)
  return [] if body.nil? || body.empty?

  data = safe_json_parse(body)
  return [] if data.nil?

  extract_raw_reports(data, content_type).filter_map do |raw|
    Report.from_raw(raw)
  end
end

.safe_json_parse(body) ⇒ Object?

Parse JSON, swallowing the errors a hostile/garbled body can throw.

Parameters:

  • body (String)

Returns:

  • (Object, nil)

    the parsed structure, or nil on any parse error.



57
58
59
60
61
# File 'lib/otto/security/csp/parser.rb', line 57

def safe_json_parse(body)
  JSON.parse(body)
rescue JSON::ParserError, EncodingError
  nil
end