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]

Reply via email to