Repository: zopefoundation/Zope (language: python)

Issue:
TypeError: HTTPRequest fails when processing input text records that need tainting and their position is after file type records
## BUG/PROBLEM REPORT / FEATURE REQUEST

A `TypeError: can't pickle cStringIO.StringO objects` is raised when `ZPublisher` processes a request input record with a value that contains a character that needs to be tainted (e.g. "<") and when a record from "file" type has been processed previously. 

When a value from a record should be tainted, `ZPublisher` does it, but it also does a `deepcopy`of the records processed earlier here: https://github.com/zopefoundation/Zope/blob/53d8f7e0d2e30a66984b1380dbe52c1fff764139/src/ZPublisher/HTTPRequest.py#L725-L726 . Thus, if one (or more) of these records processed earlier is from file type, `deepcopy` cannot do the job and the following traceback arises:

```
Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 162, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 371, in publish_module
  Module ZPublisher.WSGIPublisher, line 232, in publish
  Module ZPublisher.HTTPRequest, line 737, in processInputs
  Module copy, line 163, in deepcopy
  Module copy, line 230, in _deepcopy_list
  Module copy, line 190, in deepcopy
  Module copy, line 334, in _reconstruct
  Module copy, line 163, in deepcopy
  Module copy, line 257, in _deepcopy_dict
  Module copy, line 190, in deepcopy
  Module copy, line 334, in _reconstruct
  Module copy, line 163, in deepcopy
  Module copy, line 257, in _deepcopy_dict
  Module copy, line 182, in deepcopy
TypeError: can't pickle cStringIO.StringO objects
```

As a result, the base error screen from plone "We’re sorry, but there seems to be an error…" is rendered, but without any error or traceback information.

### What I did:

Tried to submit a form made of the following:

```html
<form name="set-analysis-conditions-form"
                class="form"
                method="POST"
                enctype="multipart/form-data"
                tal:attributes="action string:${here/absolute_url}/set_analysis_conditions">

            <table class="table table-sm small">
              <colgroup>
                <col style="width:20%"/>
              </colgroup>
              <tal:condition repeat="condition python:view.get_conditions()">
                <tr tal:define="required python:condition.get('required') == 'on';
                                is_checkbox python:condition.get('type') == 'checkbox';
                                required python:required and not is_checkbox;">
                  <td>
                    <label class="formQuestion">
                      <span tal:content="structure condition/title"/>
                      <span class="fieldRequired"
                            tal:condition="required"
                            i18n:attributes="title title_required;"
                            title="Required">&nbsp;</span>
                      <span class="formHelp discreet help-block-small"
                            tal:content="structure condition/description|nothing"/>
                    </label>
                  </td>
                  <td tal:define="options condition/options|nothing">

                    <!-- Hidden fields to keep original conditions info -->
                    <input type="hidden" name="conditions.title:records"
                           tal:attributes="value condition/title"/>
                    <input type="hidden" name="conditions.description:records"
                           tal:attributes="value condition/description"/>
                    <input type="hidden" name="conditions.choices:records"
                           tal:attributes="value condition/choices"/>
                    <input type="hidden" name="conditions.uid:records"
                           tal:attributes="value condition/uid"/>
                    <input type="hidden" name="conditions.type:records"
                           tal:attributes="value condition/type"/>
                    <input type="hidden" name="conditions.required:records"
                           tal:attributes="value condition/required"/>
                    <input type="hidden" name="conditions.attachment:records"
                           tal:attributes="value condition/attachment/uid|nothing"/>

                    <select tal:condition="options"
                            tal:define="value condition/value|nothing"
                            name="conditions.value:records">
                      <option tal:condition="not:required"/>
                      <tal:option repeat="option options">
                        <option tal:content="option"
                                tal:condition="python:option == value"
                                selected="selected"/>
                        <option tal:content="option"
                                tal:condition="python:option != value" />
                      </tal:option>
                    </select>

                    <tal:non_select condition="not:options">
                      <input tal:attributes="type condition/type;
                                             value condition/value"
                             tal:condition="required"
                             required="required"
                             name="conditions.value:records"/>

                      <input tal:attributes="type condition/type;
                                             value condition/value"
                             tal:condition="not:required"
                             name="conditions.value:records"/>
                    </tal:non_select>

                    <div tal:define="attachment condition/attachment|nothing"
                         tal:condition="attachment" class="mt-2">

                      <a tal:content="attachment/filename"
                         tal:attributes="href attachment/download_url"/>
                    </div>

                  </td>
                </tr>
              </tal:condition>
            </table>


            <div class="form-group mt-2">
              <input class="btn btn-sm btn-primary"
                     type="submit"
                     name="set_conditions"
                     i18n:attributes="value"
                     value="Set Conditions" />
            </div>

            <!-- hidden fields -->
            <input type="hidden" name="submitted" value="1" />
            <input type="hidden" name="uid"
                   tal:attributes="value python:view.get_uid_from_request()" />
            <input tal:replace="structure context/@@authenticator/authenticator"/>

          </form>
```


### What I expect to happen:

No traceback, records are processed correctly

### What actually happened:

```
Traceback (innermost last):
  Module ZPublisher.WSGIPublisher, line 162, in transaction_pubevents
  Module ZPublisher.WSGIPublisher, line 371, in publish_module
  Module ZPublisher.WSGIPublisher, line 232, in publish
  Module ZPublisher.HTTPRequest, line 737, in processInputs
  Module copy, line 163, in deepcopy
  Module copy, line 230, in _deepcopy_list
  Module copy, line 190, in deepcopy
  Module copy, line 334, in _reconstruct
  Module copy, line 163, in deepcopy
  Module copy, line 257, in _deepcopy_dict
  Module copy, line 190, in deepcopy
  Module copy, line 334, in _reconstruct
  Module copy, line 163, in deepcopy
  Module copy, line 257, in _deepcopy_dict
  Module copy, line 182, in deepcopy
TypeError: can't pickle cStringIO.StringO objects
```


### PDB session

```
TypeError: can't pickle cStringIO.StringO objects
(Pdb++) where
[...]
-> self.execute()
[6]   /home/senaite/dev/buildout-cache/eggs/waitress-1.4.4-py2.7.egg/waitress/task.py(441)execute()
-> app_iter = self.channel.server.application(environ, start_response)
[7]   /home/senaite/dev/buildout-cache/eggs/Paste-3.5.0-py2.7.egg/paste/translogger.py(69)__call__()
-> return self.application(environ, replacement_start_response)
[8]   /home/senaite/dev/buildout-cache/eggs/Zope-4.8.2-py2.7.egg/ZPublisher/httpexceptions.py(30)__call__()
-> return self.application(environ, start_response)
[9]   /home/senaite/dev/buildout-cache/eggs/Zope-4.8.2-py2.7.egg/ZPublisher/WSGIPublisher.py(371)publish_module()
-> response = _publish(request, new_mod_info)
[10]   /home/senaite/dev/buildout-cache/eggs/Zope-4.8.2-py2.7.egg/ZPublisher/WSGIPublisher.py(232)publish()
-> request.processInputs()
[11] > /home/senaite/dev/buildout-cache/eggs/Zope-4.8.2-py2.7.egg/ZPublisher/HTTPRequest.py(738)processInputs()
-> reclist)
[12]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(163)deepcopy()
-> y = copier(x, memo)
[13]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(230)_deepcopy_list()
-> y.append(deepcopy(a, memo))
[14]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(190)deepcopy()
-> y = _reconstruct(x, rv, 1, memo)
[15]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(334)_reconstruct()
-> state = deepcopy(state, memo)
[16]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(163)deepcopy()
-> y = copier(x, memo)
[17]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(257)_deepcopy_dict()
-> y[deepcopy(key, memo)] = deepcopy(value, memo)
[18]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(190)deepcopy()
-> y = _reconstruct(x, rv, 1, memo)
[19]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(334)_reconstruct()
-> state = deepcopy(state, memo)
[20]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(163)deepcopy()
-> y = copier(x, memo)
[21]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(257)_deepcopy_dict()
-> y[deepcopy(key, memo)] = deepcopy(value, memo)
[22]   /home/senaite/dev/Python-2.7/lib/python2.7/copy.py(182)deepcopy()
-> rv = reductor(2)
(Pdb++) key
'conditions'
(Pdb++) item
'<'
(Pdb++) reclist[-1]
{'attachment': '', 'choices': '', 'description': '', 'required': '', 'title': 'Comentarios TGA', 'type': 'text', 'uid': '3939c785fcce47c9a3425d7e1d28aca2'}
(Pdb++) reclist[-2]
{'attachment': '', 'choices': '', 'description': 'Si el an\xc3\xa1lisis no se ajusta al esquema anterior, adjuntar fichero con las condiciones de ensayo', 'required': '', 'title': 'Fichero adjunto', 'type': 'file', 'uid': '3939c785fcce47c9a3425d7e1d28aca2', 'value': <ZPublisher.HTTPRequest.FileUpload object at 0x7fd973388b90>}
```


### What version of Python and Zope/Addons I am using:

- Plone 5.2.9 (5217)
- CMF 2.6.0
- Zope 4.8.2
- Python 2.7.15 (default, Jun 2 2020, 22:12:19) [GCC 8.3.0]
- PIL 6.2.2 (Pillow)
- WSGI: On
- Server: waitress 1.4.4




Discussion / hints:
Jordi Puiggené wrote at 2023-1-18 02:39 -0800:
>A `TypeError: can't pickle cStringIO.StringO objects` is raised when `ZPublisher` processes a request input record with a value that contains a character that needs to be tainted (e.g. "<") and when a record from "file" type has been processed previously.

Tainting is an old security feature for DTML: when form data or cookies
are accessed implicitly via the request object,
they are (HTML) quoted on DTML rendering if they contain unsafe characters.

The tainting logic is incredibly complex and your bug report
reveals one of its holes. I have tried to streamline it in
"https://github.com/zopefoundation/Zope/pull/648", but this is not
yet merged (and might have the same problem you have observed).


Page templates no longer rely on tainting logic; they quote by
default and unquoted rendering must be called for explicitly (and
this hopefully is not done for things involving user input).


If you do not process untrusted user input with DTML objects,
you could disable the tainting logic giving the envvar
"ZOPE_DTML_REQUEST_AUTOQUOTE" e.g. the value "0".
This should avoid the bug you have reported.

Note that Zope's management interface (--> "ZMI") partially still uses DTML.
Thus, if you disable tainting, you likely must restrict the use
of large parts of the ZMI to trusted users.
A good starting point would be to restrict the permission
`View management screens` (or similarly spelled) to `Manager`.


@xispa You could try the version from #1097.
Sure, let me try
Excellent! :confetti_ball: it works properly now. Thank you very much @d-maurer for your fast response, we really appreciate it!

Fix patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 3306e8e7dd..75d31a882c 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -10,6 +10,9 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst
 5.8.1 (unreleased)
 ------------------
 
+- Sanitize tainting fixing
+  `#1095 <https://github.com/zopefoundation/Zope/issues/1095>`_
+
 - Replace ``cgi.FieldStorage`` by ``multipart`` avoiding
   the ``cgi`` module deprecated by Python 3.11.
 
diff --git a/src/ZPublisher/HTTPRequest.py b/src/ZPublisher/HTTPRequest.py
index 1c13ca51d3..d9f2136771 100644
--- a/src/ZPublisher/HTTPRequest.py
+++ b/src/ZPublisher/HTTPRequest.py
@@ -20,13 +20,12 @@
 import random
 import re
 import time
-from copy import deepcopy
 from types import SimpleNamespace
 from urllib.parse import parse_qsl
 from urllib.parse import unquote
 from urllib.parse import urlparse
 
-from AccessControl.tainted import should_be_tainted
+from AccessControl.tainted import should_be_tainted as base_should_be_tainted
 from AccessControl.tainted import taint_string
 from multipart import Headers
 from multipart import MultipartParser
@@ -446,22 +445,11 @@ def __init__(self, stdin, environ, response, clean=0):
         # vars with the same name - they are more like default values
         # for names not otherwise specified in the form.
         cookies = {}
-        taintedcookies = {}
         k = get_env('HTTP_COOKIE', '')
         if k:
             parse_cookie(k, cookies)
-            for k, v in cookies.items():
-                istainted = 0
-                if should_be_tainted(k):
-                    k = taint_string(k)
-                    istainted = 1
-                if should_be_tainted(v):
-                    v = taint_string(v)
-                    istainted = 1
-                if istainted:
-                    taintedcookies[k] = v
         self.cookies = cookies
-        self.taintedcookies = taintedcookies
+        self.taintedcookies = taint(cookies)
 
     def processInputs(
             self,
@@ -497,7 +485,6 @@ def processInputs(
 
         form = self.form
         other = self.other
-        taintedform = self.taintedform
 
         # If 'QUERY_STRING' is not present in environ
         # FieldStorage will try to get it from sys.argv[1]
@@ -531,7 +518,6 @@ def processInputs(
             fslist = fs.list
             tuple_items = {}
             defaults = {}
-            tainteddefaults = {}
             converter = None
 
             for item in fslist:  # form data
@@ -562,7 +548,6 @@ def processInputs(
                 # they cannot be encoded with a standard error handler.
                 # We might want to prevent this.
                 character_encoding = ''  # currently used encoding
-                isFileUpload = 0
                 key = item.name
                 if key is None:
                     continue
@@ -572,7 +557,6 @@ def processInputs(
                    hasattr(item, 'filename') and \
                    hasattr(item, 'headers'):
                     item = FileUpload(item, self.charset)
-                    isFileUpload = 1
                 else:
                     character_encoding = self.charset
                     item = item.value.decode(
@@ -583,10 +567,6 @@ def processInputs(
                 # `key` the field name (`str`)
 
                 flags = 0
-                # Variables for potentially unsafe values.
-                tainted = None
-                converter_type = None
-
                 # Loop through the different types and set
                 # the appropriate flags
 
@@ -610,7 +590,6 @@ def processInputs(
 
                         if c is not None:
                             converter = c
-                            converter_type = type_name
                             flags = flags | CONVERTED
                         elif type_name == 'list':
                             flags = flags | SEQUENCE
@@ -640,7 +619,8 @@ def processInputs(
                                 flags = flags | EMPTY
                         elif has_codec(type_name):
                             # recode:
-                            assert not isFileUpload, "cannot recode files"
+                            assert not isinstance(item, FileUpload), \
+                                   "cannot recode files"
                             item = item.encode(
                                 character_encoding, "surrogateescape")
                             character_encoding = type_name
@@ -662,11 +642,6 @@ def processInputs(
                 if key in isCGI_NAMEs or key.startswith('HTTP_'):
                     continue
 
-                # If the key is tainted, mark it so as well.
-                tainted_key = key
-                if should_be_tainted(key):
-                    tainted_key = taint_string(key)
-
                 if flags:
 
                     # skip over empty fields
@@ -678,17 +653,6 @@ def processInputs(
                         key = key.split(".")
                         key, attr = ".".join(key[:-1]), key[-1]
 
-                        # Update the tainted_key if necessary
-                        tainted_key = key
-                        if should_be_tainted(key):
-                            tainted_key = taint_string(key)
-
-                        # Attributes cannot hold a <.
-                        if should_be_tainted(attr):
-                            raise ValueError(
-                                "%s is not a valid record attribute name" %
-                                html.escape(attr, True))
-
                     # defer conversion
                     if flags & CONVERTED:
                         try:
@@ -698,23 +662,6 @@ def processInputs(
                                                    "surrogateescape")
                             item = converter(item)
 
-                            # Flag potentially unsafe values
-                            if converter_type in ('string', 'required', 'text',
-                                                  'ustring', 'utext'):
-                                if not isFileUpload and \
-                                   should_be_tainted(item):
-                                    tainted = taint_string(item)
-                            elif converter_type in ('tokens', 'lines',
-                                                    'utokens', 'ulines'):
-                                is_tainted = 0
-                                tainted = item[:]
-                                for i in range(len(tainted)):
-                                    if should_be_tainted(tainted[i]):
-                                        is_tainted = 1
-                                        tainted[i] = taint_string(tainted[i])
-                                if not is_tainted:
-                                    tainted = None
-
                         except Exception:
                             if not item and \
                                not (flags & DEFAULT) and \
@@ -724,31 +671,14 @@ def processInputs(
                                     item = getattr(item, attr)
                                 if flags & RECORDS:
                                     item = getattr(item[-1], attr)
-                                if tainted_key in tainteddefaults:
-                                    tainted = tainteddefaults[tainted_key]
-                                    if flags & RECORD:
-                                        tainted = getattr(tainted, attr)
-                                    if flags & RECORDS:
-                                        tainted = getattr(tainted[-1], attr)
                             else:
                                 raise
 
-                    elif not isFileUpload and should_be_tainted(item):
-                        # Flag potentially unsafe values
-                        tainted = taint_string(item)
-
-                    # If the key is tainted, we need to store stuff in the
-                    # tainted dict as well, even if the value is safe.
-                    if should_be_tainted(tainted_key) and tainted is None:
-                        tainted = item
-
                     # Determine which dictionary to use
                     if flags & DEFAULT:
                         mapping_object = defaults
-                        tainted_mapping = tainteddefaults
                     else:
                         mapping_object = form
-                        tainted_mapping = taintedform
 
                     # Insert in dictionary
                     if key in mapping_object:
@@ -757,49 +687,6 @@ def processInputs(
                             # in the list. reclist is mutable.
                             reclist = mapping_object[key]
                             x = reclist[-1]
-
-                            if tainted:
-                                # Store a tainted copy as well
-                                if tainted_key not in tainted_mapping:
-                                    tainted_mapping[tainted_key] = deepcopy(
-                                        reclist)
-                                treclist = tainted_mapping[tainted_key]
-                                lastrecord = treclist[-1]
-
-                                if not hasattr(lastrecord, attr):
-                                    if flags & SEQUENCE:
-                                        tainted = [tainted]
-                                    setattr(lastrecord, attr, tainted)
-                                else:
-                                    if flags & SEQUENCE:
-                                        getattr(
-                                            lastrecord, attr).append(tainted)
-                                    else:
-                                        newrec = record()
-                                        setattr(newrec, attr, tainted)
-                                        treclist.append(newrec)
-
-                            elif tainted_key in tainted_mapping:
-                                # If we already put a tainted value into this
-                                # recordset, we need to make sure the whole
-                                # recordset is built.
-                                treclist = tainted_mapping[tainted_key]
-                                lastrecord = treclist[-1]
-                                copyitem = item
-
-                                if not hasattr(lastrecord, attr):
-                                    if flags & SEQUENCE:
-                                        copyitem = [copyitem]
-                                    setattr(lastrecord, attr, copyitem)
-                                else:
-                                    if flags & SEQUENCE:
-                                        getattr(
-                                            lastrecord, attr).append(copyitem)
-                                    else:
-                                        newrec = record()
-                                        setattr(newrec, attr, copyitem)
-                                        treclist.append(newrec)
-
                             if not hasattr(x, attr):
                                 # If the attribute does not
                                 # exist, set it
@@ -836,57 +723,9 @@ def processInputs(
                                 # it is not a sequence so
                                 # set the attribute
                                 setattr(b, attr, item)
-
-                            # Store a tainted copy as well if necessary
-                            if tainted:
-                                if tainted_key not in tainted_mapping:
-                                    tainted_mapping[tainted_key] = deepcopy(
-                                        mapping_object[key])
-                                b = tainted_mapping[tainted_key]
-                                if flags & SEQUENCE:
-                                    seq = getattr(b, attr, [])
-                                    seq.append(tainted)
-                                    setattr(b, attr, seq)
-                                else:
-                                    setattr(b, attr, tainted)
-
-                            elif tainted_key in tainted_mapping:
-                                # If we already put a tainted value into this
-                                # record, we need to make sure the whole record
-                                # is built.
-                                b = tainted_mapping[tainted_key]
-                                if flags & SEQUENCE:
-                                    seq = getattr(b, attr, [])
-                                    seq.append(item)
-                                    setattr(b, attr, seq)
-                                else:
-                                    setattr(b, attr, item)
-
                         else:
                             # it is not a record or list of records
                             found = mapping_object[key]
-
-                            if tainted:
-                                # Store a tainted version if necessary
-                                if tainted_key not in tainted_mapping:
-                                    copied = deepcopy(found)
-                                    if isinstance(copied, list):
-                                        tainted_mapping[tainted_key] = copied
-                                    else:
-                                        tainted_mapping[tainted_key] = [copied]
-                                tainted_mapping[tainted_key].append(tainted)
-
-                            elif tainted_key in tainted_mapping:
-                                # We may already have encountered a tainted
-                                # value for this key, and the tainted_mapping
-                                # needs to hold all the values.
-                                tfound = tainted_mapping[tainted_key]
-                                if isinstance(tfound, list):
-                                    tainted_mapping[tainted_key].append(item)
-                                else:
-                                    tainted_mapping[tainted_key] = [tfound,
-                                                                    item]
-
                             if isinstance(found, list):
                                 found.append(item)
                             else:
@@ -902,15 +741,6 @@ def processInputs(
                                 item = [item]
                             setattr(a, attr, item)
                             mapping_object[key] = [a]
-
-                            if tainted:
-                                # Store a tainted copy if necessary
-                                a = record()
-                                if flags & SEQUENCE:
-                                    tainted = [tainted]
-                                setattr(a, attr, tainted)
-                                tainted_mapping[tainted_key] = [a]
-
                         elif flags & RECORD:
                             # Create a new record, set its attribute
                             # and put it in the dictionary
@@ -918,63 +748,20 @@ def processInputs(
                                 item = [item]
                             r = mapping_object[key] = record()
                             setattr(r, attr, item)
-
-                            if tainted:
-                                # Store a tainted copy if necessary
-                                if flags & SEQUENCE:
-                                    tainted = [tainted]
-                                r = tainted_mapping[tainted_key] = record()
-                                setattr(r, attr, tainted)
                         else:
                             # it is not a record or list of records
                             if flags & SEQUENCE:
                                 item = [item]
                             mapping_object[key] = item
 
-                            if tainted:
-                                # Store a tainted copy if necessary
-                                if flags & SEQUENCE:
-                                    tainted = [tainted]
-                                tainted_mapping[tainted_key] = tainted
-
                 else:
                     # This branch is for case when no type was specified.
                     mapping_object = form
 
-                    if not isFileUpload and should_be_tainted(item):
-                        tainted = taint_string(item)
-                    elif should_be_tainted(key):
-                        tainted = item
-
                     # Insert in dictionary
                     if key in mapping_object:
                         # it is not a record or list of records
                         found = mapping_object[key]
-
-                        if tainted:
-                            # Store a tainted version if necessary
-                            if tainted_key not in taintedform:
-                                copied = deepcopy(found)
-                                if isinstance(copied, list):
-                                    taintedform[tainted_key] = copied
-                                else:
-                                    taintedform[tainted_key] = [copied]
-                            elif not isinstance(
-                                    taintedform[tainted_key], list):
-                                taintedform[tainted_key] = [
-                                    taintedform[tainted_key]]
-                            taintedform[tainted_key].append(tainted)
-
-                        elif tainted_key in taintedform:
-                            # We may already have encountered a tainted value
-                            # for this key, and the taintedform needs to hold
-                            # all the values.
-                            tfound = taintedform[tainted_key]
-                            if isinstance(tfound, list):
-                                taintedform[tainted_key].append(item)
-                            else:
-                                taintedform[tainted_key] = [tfound, item]
-
                         if isinstance(found, list):
                             found.append(item)
                         else:
@@ -982,54 +769,20 @@ def processInputs(
                             mapping_object[key] = found
                     else:
                         mapping_object[key] = item
-                        if tainted:
-                            taintedform[tainted_key] = tainted
 
             # insert defaults into form dictionary
             if defaults:
                 for key, value in defaults.items():
-                    tainted_key = key
-                    if should_be_tainted(key):
-                        tainted_key = taint_string(key)
-
                     if key not in form:
                         # if the form does not have the key,
                         # set the default
                         form[key] = value
-
-                        if tainted_key in tainteddefaults:
-                            taintedform[tainted_key] = \
-                                tainteddefaults[tainted_key]
                     else:
                         # The form has the key
-                        tdefault = tainteddefaults.get(tainted_key, value)
                         if isinstance(value, record):
                             # if the key is mapped to a record, get the
                             # record
                             r = form[key]
-
-                            # First deal with tainted defaults.
-                            if tainted_key in taintedform:
-                                tainted = taintedform[tainted_key]
-                                for k, v in tdefault.__dict__.items():
-                                    if not hasattr(tainted, k):
-                                        setattr(tainted, k, v)
-
-                            elif tainted_key in tainteddefaults:
-                                # Find out if any of the tainted default
-                                # attributes needs to be copied over.
-                                missesdefault = 0
-                                for k, v in tdefault.__dict__.items():
-                                    if not hasattr(r, k):
-                                        missesdefault = 1
-                                        break
-                                if missesdefault:
-                                    tainted = deepcopy(r)
-                                    for k, v in tdefault.__dict__.items():
-                                        if not hasattr(tainted, k):
-                                            setattr(tainted, k, v)
-                                    taintedform[tainted_key] = tainted
-
                             for k, v in value.__dict__.items():
                                 # loop through the attributes and value
                                 # in the default dictionary
@@ -1044,56 +797,6 @@ def processInputs(
                             val = form[key]
                             if not isinstance(val, list):
                                 val = [val]
-
-                            # First deal with tainted copies
-                            if tainted_key in taintedform:
-                                tainted = taintedform[tainted_key]
-                                if not isinstance(tainted, list):
-                                    tainted = [tainted]
-                                for defitem in tdefault:
-                                    if isinstance(defitem, record):
-                                        for k, v in defitem.__dict__.items():
-                                            for origitem in tainted:
-                                                if not hasattr(origitem, k):
-                                                    setattr(origitem, k, v)
-                                    else:
-                                        if defitem not in tainted:
-                                            tainted.append(defitem)
-                                taintedform[tainted_key] = tainted
-
-                            elif tainted_key in tainteddefaults:
-                                missesdefault = 0
-                                for defitem in tdefault:
-                                    if isinstance(defitem, record):
-                                        try:
-                                            for k, v in \
-                                                    defitem.__dict__.items():
-                                                for origitem in val:
-                                                    if not hasattr(
-                                                            origitem, k):
-                                                        missesdefault = 1
-                                                        raise NestedLoopExit
-                                        except NestedLoopExit:
-                                            break
-                                    else:
-                                        if defitem not in val:
-                                            missesdefault = 1
-                                            break
-                                if missesdefault:
-                                    tainted = deepcopy(val)
-                                    for defitem in tdefault:
-                                        if isinstance(defitem, record):
-                                            for k, v in (
-                                                    defitem.__dict__.items()):
-                                                for origitem in tainted:
-                                                    if not hasattr(
-                                                            origitem, k):
-                                                        setattr(origitem, k, v)
-                                        else:
-                                            if defitem not in tainted:
-                                                tainted.append(defitem)
-                                    taintedform[tainted_key] = tainted
-
                             for x in value:
                                 # for each x in the list
                                 if isinstance(x, record):
@@ -1140,9 +843,6 @@ def processInputs(
                     attr = new
                     if k in form:
                         # If the form has the split key get its value
-                        tainted_split_key = k
-                        if should_be_tainted(k):
-                            tainted_split_key = taint_string(k)
                         item = form[k]
                         if isinstance(item, record):
                             # if the value is mapped to a record, check if it
@@ -1160,24 +860,8 @@ def processInputs(
                                     # convert it to a tuple and set it
                                     value = tuple(getattr(x, attr))
                                     setattr(x, attr, value)
-
-                        # Do the same for the tainted counterpart
-                        if tainted_split_key in taintedform:
-                            tainted = taintedform[tainted_split_key]
-                            if isinstance(item, record):
-                                seq = tuple(getattr(tainted, attr))
-                                setattr(tainted, attr, seq)
-                            else:
-                                for trec in tainted:
-                                    if hasattr(trec, attr):
-                                        seq = getattr(trec, attr)
-                                        seq = tuple(seq)
-                                        setattr(trec, attr, seq)
                     else:
                         # the form does not have the split key
-                        tainted_key = key
-                        if should_be_tainted(key):
-                            tainted_key = taint_string(key)
                         if key in form:
                             # if it has the original key, get the item
                             # convert it to a tuple
@@ -1185,9 +869,7 @@ def processInputs(
                             item = tuple(form[key])
                             form[key] = item
 
-                        if tainted_key in taintedform:
-                            tainted = tuple(taintedform[tainted_key])
-                            taintedform[tainted_key] = tainted
+            self.taintedform = taint(self.form)
 
         if meth:
             if 'PATH_INFO' in environ:
@@ -1952,3 +1634,63 @@ def _filterPasswordFields(items):
 def use_builtin_xmlrpc(request):
     checker = queryUtility(IXmlrpcChecker)
     return checker is None or checker(request)
+
+
+def taint(d):
+    """return as ``dict`` the items from *d* which require tainting.
+
+    *d* must be a ``dict`` with ``str`` keys and values recursively
+    build from elementary values, ``list``, ``tuple``, ``record`` and
+    ``dict``.
+    """
+    def should_taint(v):
+        """check whether *v* needs tainting."""
+        if isinstance(v, (list, tuple)):
+            return any(should_taint(x) for x in v)
+        if isinstance(v, (record, dict)):
+            for k in v:
+                if should_taint(k):
+                    return True
+                if should_taint(v[k]):
+                    return True
+        return should_be_tainted(v)
+
+    def _taint(v):
+        """return a copy of *v* with tainted replacements.
+
+        In the copy, each ``str`` which requires tainting
+        is replaced by the corresponding ``taint_string``.
+        """
+        __traceback_info__ = v
+        if isinstance(v, (bytes, str)) and should_be_tainted(v):
+            return taint_string(v)
+        if isinstance(v, (list, tuple)):
+            return v.__class__(_taint(x) for x in v)
+        if isinstance(v, record):
+            rn = record()
+            for k in v:
+                __traceback_info__ = v, k
+                if should_be_tainted(k):
+                    raise ValueError("Cannot taint `record` attribute names")
+                setattr(rn, k, _taint(v[k]))
+            return rn
+        if isinstance(v, dict):
+            return v.__class__((_taint(k), _taint(v[k])) for k in v)
+        return v
+    td = {}
+    for k in d:
+        v = d[k]
+        if should_taint(k) or should_taint(v):
+            td[_taint(k)] = _taint(v)
+    return td
+
+
+def should_be_tainted(v):
+    """Work around ``AccessControl`` weakness."""
+    if isinstance(v, FileUpload):
+        # `base_should_be_tainted` would read `v` as a side effect
+        return False
+    try:
+        return base_should_be_tainted(v)
+    except Exception:
+        return False


Test patch:
diff --git a/src/ZPublisher/tests/testHTTPRequest.py b/src/ZPublisher/tests/testHTTPRequest.py
index 377eb751e1..0c4bb74a5e 100644
--- a/src/ZPublisher/tests/testHTTPRequest.py
+++ b/src/ZPublisher/tests/testHTTPRequest.py
@@ -18,6 +18,7 @@
 from io import BytesIO
 from unittest.mock import patch
 
+from AccessControl.tainted import TaintedString
 from AccessControl.tainted import should_be_tainted
 from zExceptions import NotFound
 from zope.component import getGlobalSiteManager
@@ -28,6 +29,7 @@
 from zope.publisher.interfaces.http import IHTTPRequest
 from zope.testing.cleanup import cleanUp
 from ZPublisher.HTTPRequest import BadRequest
+from ZPublisher.HTTPRequest import FileUpload
 from ZPublisher.HTTPRequest import search_type
 from ZPublisher.interfaces import IXmlrpcChecker
 from ZPublisher.tests.testBaseRequest import TestRequestViewsBase
@@ -1163,6 +1165,13 @@ class IFoo(Interface):
         self.assertTrue(IFoo.providedBy(clone))
 
     def test_resolve_url_doesnt_send_endrequestevent(self):
+        # The following imports are necessary:
+        #  They happen implicitely in `request.resolve_url`
+        #  They creates `zope.schema` events
+        # Doing them here avoids those unrelated events
+        import OFS.PropertyManager  # noqa: F401
+        import OFS.SimpleItem  # noqa: F401
+        #
         import zope.event
         events = []
         zope.event.subscribers.append(events.append)
@@ -1323,6 +1332,19 @@ def test_bytes_converter(self):
         req.processInputs()
         self.assertEqual(req.form["a"], val)
 
+    def test_issue_1095(self):
+        body = TEST_ISSUE_1095_DATA
+        env = self._makePostEnviron(body)
+        req = self._makeOne(BytesIO(body), env)
+        req.processInputs()
+        r = req["r"]
+        self.assertEqual(len(r), 2)
+        self.assertIsInstance(r[0].x, FileUpload)
+        self.assertIsInstance(r[1].x, str)
+        r = req.taintedform["r"]
+        self.assertIsInstance(r[0].x, FileUpload)
+        self.assertIsInstance(r[1].x, TaintedString)
+
 
 class TestHTTPRequestZope3Views(TestRequestViewsBase):
 
@@ -1405,3 +1427,19 @@ def test_special(self):
 
 --12345--
 ''' % (b'test' * 1000)
+
+TEST_ISSUE_1095_DATA = b'''
+--12345
+Content-Disposition: form-data; name="r.x:records"; filename="fn"
+Content-Type: application/octet-stream
+
+test
+
+--12345
+Content-Disposition: form-data; name="r.x:records"
+Content-Type: text/html
+
+<body>abc</body>
+
+--12345--
+'''