Ritesh Shukla created HDDS-17632:
------------------------------------
Summary: S3G: prefixed elements in request XML are ignored; a
DeleteObjects <s3:Key> is read as a missing key
Key: HDDS-17632
URL: https://issues.apache.org/jira/browse/HDDS-17632
Project: Apache Ozone
Issue Type: Bug
Components: S3
Reporter: Ritesh Shukla
Assignee: Ritesh Shukla
h3. Problem
In XML an element can carry a namespace prefix. {{<s3:Key>}} with
{{xmlns:s3="http://s3.amazonaws.com/doc/2006-03-01/"}} is the same {{Key}}
element as an unprefixed {{<Key>}} in the S3 namespace. Tested against AWS S3
on 2026-10-31, S3 reads an element whose prefix is bound to the S3 namespace,
or whose prefix is not declared at all, as the plain element: it deletes the
key named by a prefixed {{Key}}, and accepts DeleteObjects, PutObjectTagging
and CompleteMultipartUpload bodies in which the elements are prefixed. It
refuses an element whose prefix is bound to another namespace, even on its own,
and a name with two colons, with 400 {{MalformedXML}}. A change of default
namespace ({{xmlns="..."}}) is ignored.
The S3 Gateway matches elements by their raw name, prefix included. To the
gateway, {{s3:Key}} is an unknown element, so it skips it. It then acts on the
request as if that element were not there.
Example. This DeleteObjects body names one key:
{code:xml}
<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/"
xmlns:s3="http://s3.amazonaws.com/doc/2006-03-01/">
<Object><s3:Key>photos/cat.jpg</s3:Key></Object>
</Delete>
{code}
S3 answers 200 and deletes {{photos/cat.jpg}}. The gateway reads an {{Object}}
without a key. Building the request to OM then fails with a
NullPointerException. Nothing is deleted, not even the other keys in the same
request, and the client gets a server error instead of a delete result.
||DeleteObjects body||AWS S3 (tested 2026-10-31)||Ozone today||
|{{<Key>k</Key>}}, with or without {{xmlns}}|200, deletes {{k}}|same|
|{{<s3:Key>k</s3:Key>}}, {{s3}} bound to the S3 namespace|200, deletes
{{k}}|reads no key; the request fails and nothing is deleted|
|{{<s3:Key>k</s3:Key>}}, {{s3}} not declared|200, deletes {{k}}|reads no key;
the request fails and nothing is deleted|
|every element prefixed: {{<s3:Delete><s3:Object><s3:Key>k</s3:Key>...}}|200,
deletes {{k}}|400 {{InvalidRequest}}: "unexpected element ... s3:Delete"|
|{{<x:Key>k</x:Key>}} alone, {{x}} bound to {{urn:other}}|400
{{MalformedXML}}|reads no key; the request fails and nothing is deleted|
|{{<a:b:Key>k</a:b:Key>}} (two colons)|400 {{MalformedXML}}|reads no key; the
request fails and nothing is deleted|
|{{<s3:Key>a</s3:Key><Key>b</Key>}} in one {{Object}}|400 {{MalformedXML}}:
{{Key}} given twice|reads and deletes {{b}}|
|{{<Key>a</Key><x:Key>b</x:Key>}}, {{x}} bound to {{urn:other}}|400
{{MalformedXML}}|reads and deletes {{a}}|
|a second {{<Object xmlns="urn:foreign">}}|200, deletes both keys|same|
|an {{Object}} with no {{Key}}, or with only a {{VersionId}}|400
{{MalformedXML}}|reads no key; the request fails and nothing is deleted|
Ozone's readings are measured with master's request classes, namespace filter
and unmarshallers, Hadoop's secure SAX parser and jaxb-runtime 2.3.9. The
NullPointerException is measured with the protobuf call the OM client makes.
The server error after it, and the deletes, follow from code and were not run.
The last row is not about prefixes, but it fails the same way: any {{Object}}
that ends up without a key reaches the NullPointerException below.
The other request bodies go through the same filter. S3 accepted a fully
prefixed {{TagSet}} (and stored the tag) and a prefixed {{Part}} (and completed
the upload); ACL and lifecycle bodies were not sent to S3. Measured on the
gateway:
* PutObjectTagging and PutBucketTagging skip a prefixed {{TagSet}} or {{Tag}}.
The tag set is then missing or empty, and the request is refused with 400
{{MalformedXML}} (refusal from code).
* CompleteMultipartUpload skips prefixed {{Part}} elements. The empty part list
is refused with 400 {{MalformedXML}} (from code).
* PutBucketAcl skips prefixed {{Grant}} elements and reads an empty grant list.
From code, not run: it then replaces the bucket's ACL with an empty list and
answers 200.
* PutBucketLifecycleConfiguration checks {{s3:LifecycleConfiguration}} against
its list of allowed elements and refuses the body.
h3. Impact
* No client checked is affected. The AWS CLI 2.35.10 and the AWS SDK for Java
1.12.788 and 2.55.6 (the version Ozone builds against) never prefix S3
elements. Their bodies for all five request types were captured and read. Each
body either sends no namespace or declares the S3 namespace as the default
namespace; SDK v1 sends no namespace except on ACL bodies. Other clients were
not checked, and hand-written XML can use prefixes.
* A client that writes prefixes gets failures where S3 succeeds. DeleteObjects
fails as a whole, and a fully prefixed body is refused. Tagging, multipart
completion and lifecycle requests are refused. PutBucketAcl is the worst case:
from code, it clears the bucket's ACL instead of applying the grants.
* With a prefixed {{Key}} next to a plain one, the gateway deletes one of the
two keys where S3 refuses the request. That body also repeats an element; see
HDDS-17631.
* Permissions are not bypassed. OM still checks every key and ACL change
against the caller's rights.
h3. Cause
* {{MessageUnmarshaller.java:55}} gets its parser from
{{XMLUtils.newSecureSAXParserFactory()}}, which is not namespace-aware. Such a
parser reports an element only by its raw name, such as {{s3:Key}}, and leaves
the local name empty.
* {{XmlNamespaceFilter.java:46}} and {{XmlNamespaceFilter.java:52}} put every
element in the S3 namespace but pass its names on unchanged. JAXB then matches
the raw name: for the fully prefixed body, its error reports the element
{{s3:Delete}}.
* The request classes bind plain names, such as {{@XmlElement(name = "Key")}}
at {{MultiDeleteRequest.java:66-67}}. Nothing binds {{s3:Key}}, and JAXB skips
unknown elements without an error. A prefixed root element matches no root,
which {{MessageUnmarshaller.java:85}} reports as {{InvalidRequest}}.
* {{PutBucketLifecycleConfigurationUnmarshaller.java:68}} checks the same raw
name against its allowed elements.
* After the missing key (from code, except the exception):
{{endpoint/BucketEndpoint.java:402}} adds it to the key list as null.
{{OzoneManagerProtocolClientSideTranslatorPB.java:1055}} passes the list to
protobuf's {{addAllKeys}}, which throws a NullPointerException for the null
entry (measured). No request reaches OM. {{BucketEndpoint.multiDelete}} catches
only {{IOException}} there ({{endpoint/BucketEndpoint.java:427}}), so the
exception escapes it with no per-key error and no audit entry, and no exception
mapper turns it into an S3 error.
* After the empty grant list (from code): {{BucketAclHandler.java:156}} turns
it into an empty ACL list, and {{BucketAclHandler.java:177}} sets that list on
the bucket. OM replaces the bucket's ACLs with it
({{OMBucketSetAclRequest.java:70}}, {{AclListBuilder.java:95}}).
h3. Proposed fix
Answer as S3 does (tested 2026-10-31). Keep the parser as it is; the filter
tracks prefix declarations itself, since the parser reports {{xmlns:p}} as an
ordinary attribute.
* In {{XmlNamespaceFilter.startElement}} and {{endElement}}, keep a stack of
the prefixes declared on open elements. An element whose prefix is bound to the
S3 namespace, or not declared at all, is passed on under its local name, the
raw name without the prefix.
* Refuse with 400 {{MalformedXML}} an element whose prefix is bound to any
other namespace, and a name with more than one colon.
* Keep putting every element in the S3 namespace. A default namespace
declaration stays uninterpreted, as in S3, which deleted an {{Object}} declared
in {{urn:foreign}}.
* Use the same local name in the lifecycle element check.
* Refuse a DeleteObjects {{Object}} that has no {{Key}} with 400
{{MalformedXML}} before OM is called, as S3 does, so a missing key never
reaches the OM client.
* Leave attributes alone. The ACL model binds the raw attribute name
{{xsi:type}} ({{S3BucketAcl.java:181}}), and that keeps working.
A prefixed {{Key}} next to a plain one is {{Key}} given twice, and S3 answers
400 {{MalformedXML}} (tested). HDDS-17631 refuses repeated single-valued
elements the same way. After this fix such a body reaches that check as two
{{Key}} elements, so it is refused as S3 refuses it. Every check in the filter
that looks at element names must use this same local name: the lifecycle check,
the repeated-element check, and the HDDS-17600 check for elements inside a
value. Otherwise a prefix gets a body past them.
Not tested against S3: prefixes in ACL and lifecycle bodies. Until they are,
apply the same rule. If S3 answers differently, follow S3.
Why not the obvious alternatives (both measured):
* Make the parser namespace-aware. That is the usual way to get local names,
but it changes more than names. It refuses a prefix that has no declaration,
which S3 accepts (tested). It also reports {{xsi:type}} as an attribute in the
XML Schema instance namespace, so the raw-name binding stops matching. On
{{groupAccessControlList.xml}}, a test resource that follows AWS's PutBucketAcl
request example, every {{Group}} and {{AmazonCustomerByEmail}} grantee is then
read as {{CanonicalUser}}.
* Strip every prefix. That accepts {{<x:Key>}} bound to another namespace,
which S3 refuses.
* Refuse elements in another default namespace. S3 accepts them.
h3. Testing
* Unit: a new {{TestXmlNamespaceFilter}}, with S3's tested answers as the
expected results. A first prototype (local names without prefix tracking) was
run against master: 9 of its 13 cases failed before the fix and all passed
after. The prefix tracking and the two refusals below come from the later S3
test and are not prototyped yet.
** Read by local name (before: no key, a missing or empty list, or a refusal):
a prefixed {{Key}}; a fully prefixed body; a prefix with no declaration;
prefixed {{Tag}}, {{Part}} and {{Grant}} elements; a fully prefixed lifecycle
body.
** Refused with 400 {{MalformedXML}}: {{x:Key}} with {{x}} bound to another
namespace, alone and next to a plain {{Key}}; {{a:b:Key}}; an {{Object}} with
no {{Key}}.
** A prefixed unknown lifecycle element is still refused, and the error names
{{Foo}}, not {{s3:Foo}}.
** Unchanged before and after: a body with no namespace, a body in the S3
default namespace, an {{Object}} in {{urn:other}} that is still read (as S3
does), and the grantee types in {{groupAccessControlList.xml}}.
* With the prototype, all s3gateway unit tests under
{{org.apache.hadoop.ozone.s3*}} pass: 1,882 tests, including {{TestBucketAcl}}
with both ACL test resources and {{TestMultiDeleteRequestUnmarshaller}}'s
bodies with and without a namespace.
* Integration, optional, in {{AbstractS3SDKV1Tests}}: send the example body
through a presigned DeleteObjects URL, the way the suite already sends a
hand-written multipart completion body. Expected today: a server error, and the
key is kept. After the fix: 200, and the key is deleted.
----
🤖 Generated with [Claude Code|https://claude.com/claude-code]
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]