Repository: borgbackup/borg

Issue:
HashIndex collision DoS for encryption=none repos
As noted in PR #2606, the hash function of HashIndex is truncating the keys. This is generally not a problem, because ALL keys are the output of a cryptographic hash or MAC.

If encryption is disabled (--encryption=none) and an attacker can add files / chunks, then a DoS might be possible by forcing many collisions in the hash table, since the plain SHA-256 ID hash is easily calculated.

This does only affect none mode (not authenticated), since all other modes use a keyed hash / MAC, where this is not possible without knowledge of the secret key.

**Resolution:** Document, nudge towards authenticated / enabling encryption instead of `none`.


Discussion:

Resolving patch:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -2449,16 +2449,23 @@ def define_common_options(add_common_option):
         Encryption modes
         ++++++++++++++++
 
+        .. nanorst: inline-fill
+
         +----------+---------------+------------------------+--------------------------+
         | Hash/MAC | Not encrypted | Not encrypted,         | Encrypted (AEAD w/ AES)  |
         |          | no auth       | but authenticated      | and authenticated        |
         +----------+---------------+------------------------+--------------------------+
-        | SHA-256  | none          | authenticated          | repokey, keyfile         |
+        | SHA-256  | none          | `authenticated`        | repokey                  |
+        |          |               |                        | keyfile                  |
         +----------+---------------+------------------------+--------------------------+
-        | BLAKE2b  | n/a           | authenticated-blake2   | repokey-blake2,          |
-        |          |               |                        | keyfile-blake2           |
+        | BLAKE2b  | n/a           | `authenticated-blake2` | `repokey-blake2`         |
+        |          |               |                        | `keyfile-blake2`         |
         +----------+---------------+------------------------+--------------------------+
 
+        .. nanorst: inline-replace
+
+        `Marked modes` are new in Borg 1.1 and are not backwards-compatible with Borg 1.0.x.
+
         On modern Intel/AMD CPUs (except very cheap ones), AES is usually
         hardware-accelerated.
         BLAKE2b is faster than SHA256 on Intel/AMD 64-bit CPUs
@@ -2491,7 +2498,8 @@ def define_common_options(add_common_option):
 
         `none` mode uses no encryption and no authentication. It uses SHA256 as chunk
         ID hash. Not recommended, rather consider using an authenticated or
-        authenticated/encrypted mode.
+        authenticated/encrypted mode. This mode has possible denial-of-service issues
+        when running ``borg create`` on contents controlled by an attacker.
         Use it only for new repositories where no encryption is wanted **and** when compatibility
         with 1.0.x is important. If compatibility with 1.0.x is not important, use
         `authenticated-blake2` or `authenticated` instead.
diff --git a/src/borg/nanorst.py b/src/borg/nanorst.py
--- a/src/borg/nanorst.py
+++ b/src/borg/nanorst.py
@@ -58,6 +58,7 @@ def rst_to_text(text, state_hook=None, references=None):
     state_hook = state_hook or (lambda old_state, new_state, out: None)
     references = references or {}
     state = 'text'
+    inline_mode = 'replace'
     text = TextPecker(text)
     out = io.StringIO()
 
@@ -117,17 +118,26 @@ def rst_to_text(text, state_hook=None, references=None):
                 directive, is_directive, arguments = text.readline().partition('::')
                 text.read(1)
                 if not is_directive:
+                    # partition: if the separator is not in the text, the leftmost output is the entire input
+                    if directive == 'nanorst: inline-fill':
+                        inline_mode = 'fill'
+                    elif directive == 'nanorst: inline-replace':
+                        inline_mode = 'replace'
                     continue
                 process_directive(directive, arguments.strip(), out, state_hook)
                 continue
         if state in inline_single and char == state:
             state_hook(state, 'text', out)
             state = 'text'
+            if inline_mode == 'fill':
+                out.write(2 * ' ')
             continue
         if state == '``' and char == next == '`':
             state_hook(state, 'text', out)
             state = 'text'
             text.read(1)
+            if inline_mode == 'fill':
+                out.write(4 * ' ')
             continue
         if state == '**' and char == next == '*':
             state_hook(state, 'text', out)


Test patch: