From 7ff9eeee2b9f43ef62b0c6413b10198f34edcd1f Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 29 Jul 2024 14:22:07 -0400 Subject: [PATCH 1/2] update dependencies --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2b05e6..987e397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -117,7 +117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -130,7 +130,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -186,7 +186,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -268,9 +268,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets", @@ -321,7 +321,7 @@ checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -532,7 +532,7 @@ dependencies = [ "quote", "regex", "shell-words", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -681,9 +681,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -698,22 +698,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -724,9 +724,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -815,5 +815,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] From 964156701e925e17d578fc5a3b7f0924987296e0 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 29 Jul 2024 14:22:23 -0400 Subject: [PATCH 2/2] Stop operating handlers on deleted elements --- ext/selma/src/rewriter.rs | 4 ++ lib/selma/version.rb | 2 +- test/fixtures/deleting_content.html | 99 +++++++++++++++++++++++++++++ test/selma_maliciousness_test.rb | 55 ++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/deleting_content.html diff --git a/ext/selma/src/rewriter.rs b/ext/selma/src/rewriter.rs index 7cfa4d9..b78d2fc 100644 --- a/ext/selma/src/rewriter.rs +++ b/ext/selma/src/rewriter.rs @@ -515,6 +515,10 @@ impl SelmaRewriter { })); } + if element.removed() { + return Ok(()); + } + let rb_element = SelmaHTMLElement::new(element, ancestors); let rb_result = rb_handler.funcall::<_, _, Value>(Self::SELMA_HANDLE_ELEMENT, (rb_element,)); diff --git a/lib/selma/version.rb b/lib/selma/version.rb index ba95f98..a831af3 100644 --- a/lib/selma/version.rb +++ b/lib/selma/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Selma - VERSION = "0.4.3" + VERSION = "0.4.4" end diff --git a/test/fixtures/deleting_content.html b/test/fixtures/deleting_content.html new file mode 100644 index 0000000..e19806f --- /dev/null +++ b/test/fixtures/deleting_content.html @@ -0,0 +1,99 @@ + Responding to a Yetto request | Yetto Docs + + Skip to content

Responding to a Yetto request

Custom connections require an endpoint to access. This can be in your application or in one of your backend services. When setting up an endpoint for this connection, there are a few things to keep in mind:

  • The service will need to accept a GET request from Yetto with a hashed signature in the X_YETTO_SIGNATURE header.
  • The service will need to be validate the request with a signing secret. We'll give you the signing secret when you first set up the connection.
  • Your service will use the signing secret to decrypt the request parameter containing the payload from Yetto.
  • The service will need to respond to all requests within three seconds. Yetto will only display data that comes back within that time.
  • The service will need to respond to the request with valid JSON in the response body.
  • The service will need to respond to a test request during setup.
  • Yetto sends hashed data as a path parameter in the GET request. The path is appended to the URL path you set up in the webhook.

Let's walk through some of those items together.

Accepting and validating Yetto's request

When you first set up a customer connection, we'll share a signing secret with you. The signing secret will not be available on that page in the future, so be sure to copy and save it when first setting up the connection. That secret will be used to encode the payload of the GET request, the result of which can be compared to the X_YETTO_SIGNATURE header to confirm that the request came from us.

To validate the request:

  1. Get the signature value in the X_YETTO_SIGNATURE header of the Yetto GET request.
  2. Remove the beginning characters "sha256=" from the signature value; the rest of the value is the signature string you'll compare against later.
  3. Get the path parameter string of the GET request and hash it, using the signing secret as the key.
  4. Take the hex digest of the resulting hash, using the HMAC-SHA256 algorithm.
  5. Compare the hex digest you calculated to the signature value in the X_YETTO_SIGNATURE header of the request. If they match, the request is a legitimate Yetto customer connection request.

An example in Ruby on Rails might look like this:

# Get the Yetto header signature value
+yetto_signature = request.headers.fetch("X_YETTO_SIGNATURE", "")
+hmac_header = yetto_signature.split("sha256=").last
+
+# Calculate the hmac authentication digest using your signing secret
+encoded_yetto_payload =  params['splat'].first
+calculated_hmac = OpenSSL::HMAC.hexdigest(SHA256_DIGEST, SIGNING_SECRET, encoded_yetto_payload)
+
+# Compare the calculated digest to the signature value in the header
+return true if ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
+

Accept and respond to a Yetto request

Once you've validated that the request has come from Yetto, you can use the signing secret to decrypt Yetto's conversation payload. The payload is encrypted with the aes-256-gcm OpenSSL Cipher. Many languages have native implementations for decrypting these types of messages. For example, in Ruby on Rails, one can use:

ActiveSupport::MessageEncryptor.new(SIGNING_SECRET, url_safe: true, serializer: :json).decrypt_and_verify(encoded_yetto_payload)
+

The GET request parameter that Yetto sends will have data that looks like this:

{
+    "yetto": {
+        "conversation": {
+            "author": {
+                "name": "user@example.com",
+            },
+            "inbox": "ibx_01J3RB6WMJDPE5R84X0JVX3YX9",
+        },
+    }
+}
+

Use that to assemble data that you want to display in the conversation sidebar. You have three seconds to respond to Yetto's request with valid JSON. Yetto's customer connections accept JSON responses that look like this:

{
+    "version": "2024-03-06",
+    "customer": {
+        "id": "customer_12345",
+        "last_active": "10/15/2015"
+        "name": "Kelly Customer",
+        "plan_type": "Business"
+    },
+    "sections": [
+        {
+            "type": "link",
+            "title": "Link to customer details",
+            "description": "Application data",
+            "href": "https://example.com/customer/{{ data.customer.id }}"
+        },
+        {
+            "type": list,
+            "title": "List section",
+            "items": [
+                {
+                    "description": "Name",
+                    "value": "{{ data.customer.name }}"
+                },
+                {
+                    "description": "Plan type",
+                    "value": "{{ data.customer.plan_type }}"
+                },
+                {
+                    "description": "Last active",
+                    "value": "{{ data.customer.last_active }}"
+                }
+            ]
+        }
+    ]
+}
+

The version string, customer object, and sections array are required fields.

The version string is 2023-03-06. We will update our documentation if and when this changes.

The customer object can contain data to be used in the sections fields. We use Liquid templating to populate fields, so you can store data here if you want to use it across more than one section fields.

The sections array contains the data you want displayed in the conversation sidebar in Yetto. It must be an array of valid objects, with a maximum of three sections.

Currently, the section types we accept are link and list.

A link can contain any URL that you want your Support team to have access to in the Yetto conversation. This could be a link to an admin page in your system or a link to a third-party billing tool that you use. This section has the following fields:

FieldData typeRequired
descriptionstringno
hrefstringyes
titlestringno
typestringyes

Section lists

A list is an array of objects showing data that you want to see when viewing the conversation. Lists have the following fields:

FieldData typeRequired
itemsarrayyes
titlestringno
typestringyes

The items objects have following fields:

FieldData typeRequired
descriptionstringno
valuestringyes

Using the customer object

The customer object is available for you to populate and use however you need it. When referencing customer values, make sure to format them like this:

{{ data.customer.key }}
+

Pay attention to the double braces and the data.customer structure.

Responding to a test request during setup

During the initial setup of a customer connection, Yetto will send a test request to your endpoint. That request can be validated using the signing secret as all other Yetto requests will be. This test request, however, will contain a X_YETTO_RECORD_TYPE header with a value of verification. The body will be encrypted with the same aes-256-gcm OpenSSL cipher, and when decrypted, looks like this:

{
+    "yetto": {
+        "challenge": "39e34f256caed94513592cad6a89fce498da6aa1"
+    }
+}
+

Your system will need to return this challenge string to Yetto within three seconds. The test response should look like this:

{
+    "challenge": "39e34f256caed94513592cad6a89fce498da6aa1"
+}
+

Once we receive the correct challenge response from your endpoint, you'll be able to complete the [setup process](link to other doc).

diff --git a/test/selma_maliciousness_test.rb b/test/selma_maliciousness_test.rb index 6d5f2c1..1d83a41 100644 --- a/test/selma_maliciousness_test.rb +++ b/test/selma_maliciousness_test.rb @@ -223,4 +223,59 @@ def test_rewriter_does_not_halt_on_malformed_html Selma::Rewriter.new(sanitizer: sanitizer, handlers: [ContentExtractor.new]).rewrite(html) end + + class TagRemover + SELECTOR = Selma::Selector.new(match_element: "*") + + def selector + SELECTOR + end + + UNNECESSARY_TAGS = [ + "pre", + ] + + CONTENT_TO_KEEP = [ + "html", + "body", + ] + + def handle_element(element) + if UNNECESSARY_TAGS.include?(element.tag_name) + element.remove + elsif CONTENT_TO_KEEP.include?(element.tag_name) + element.remove_and_keep_content + end + end + end + + class ContentBreaker + SELECTOR = Selma::Selector.new(match_element: "*") + + def selector + SELECTOR + end + + def handle_element(element) + if Selma::Sanitizer::Config::DEFAULT[:whitespace_elements].include?(element.tag_name) && !element.removed? + element.append("\n", as: :text) + end + element.remove_and_keep_content + end + end + + def test_deleted_content_does_not_segfault + html = load_fixture("deleting_content.html") + + sanitizer_config = Selma::Sanitizer::Config::RELAXED.dup.merge({ + allow_comments: false, + allow_doctype: false, + }) + sanitizer = Selma::Sanitizer.new(sanitizer_config) + + selma = Selma::Rewriter.new(sanitizer: sanitizer, handlers: [TagRemover.new, ContentBreaker.new]) + 10.times do + selma.rewrite(html) + end + end end