This is an automated email from the ASF dual-hosted git repository.
martin-g pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 626f22d fix(schema): return Err instead of panicking on invalid
name/alias/type-ref (#547) (#548)
626f22d is described below
commit 626f22d5636ac510d3bddfa81057f35c994f16d3
Author: youichi uda <[email protected]>
AuthorDate: Tue Jun 9 04:56:17 2026 +0900
fix(schema): return Err instead of panicking on invalid name/alias/type-ref
(#547) (#548)
* fix(schema): return Err instead of panicking on invalid
name/alias/type-ref
`Schema::parse_str` panicked on schema JSON whose alias, type reference, or
nested type name is not a valid Avro identifier, because three sites in
`schema::parser` called `Name::new(..).unwrap()` / `Alias::new(..).unwrap()`
on values that already return `AvroResult`. Since `parse_str` is a public
`Result`-returning API, callers cannot guard against the unwind, so parsing
untrusted schema JSON was a DoS. This is the sibling of the `Name::parse`
fix in #496.
- `get_already_seen_schema`: an unparseable type name cannot match an
already-seen schema, so return `None` (`.ok()?`) rather than unwrap.
- `fix_aliases_namespace`: return `AvroResult<Aliases>` and propagate; an
invalid alias is a schema error (`?` added at the three call sites).
- `get_schema_type_name`: fall back to the enclosing name
(`unwrap_or(name)`),
matching the existing `_ => name` arm.
Adds a regression test (tests/avro-rs-547.rs) covering invalid aliases,
nested names and type references, plus a positive case ensuring valid
aliases still parse.
Fixes #547
Signed-off-by: youichi-uda <[email protected]>
* address review: avoid clones in fix_aliases_namespace; propagate in
get_schema_type_name
- fix_aliases_namespace: use `contains('.')` and pass `&str` /
`&format!(..)`
directly to `Alias::new`, dropping the intermediate `qualified` String and
the per-alias clones.
- get_schema_type_name: return `AvroResult<Name>` and propagate the
validation error (callers updated with `?`) instead of silently falling
back to the enclosing name on an invalid nested `type` name.
Signed-off-by: youichi-uda <[email protected]>
---------
Signed-off-by: youichi-uda <[email protected]>
Co-authored-by: youichi-uda <[email protected]>
---
avro/src/schema/parser.rs | 62 ++++++++++++++++++++++++-----------------
avro/tests/avro-rs-547.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 107 insertions(+), 26 deletions(-)
diff --git a/avro/src/schema/parser.rs b/avro/src/schema/parser.rs
index 4235209..57ef412 100644
--- a/avro/src/schema/parser.rs
+++ b/avro/src/schema/parser.rs
@@ -96,7 +96,7 @@ impl Parser {
.expect("Key unexpectedly missing");
let parsed = self.parse(&value, None)?;
self.parsed_schemas
- .insert(self.get_schema_type_name(name, &value), parsed);
+ .insert(self.get_schema_type_name(name, &value)?, parsed);
}
Ok(())
}
@@ -197,7 +197,7 @@ impl Parser {
// parsing a full schema from inside another schema. Other full schema
will not inherit namespace
let parsed = self.parse(&value, None)?;
self.parsed_schemas.insert(
- self.get_schema_type_name(fully_qualified_name, &value),
+ self.get_schema_type_name(fully_qualified_name, &value)?,
parsed.clone(),
);
@@ -522,8 +522,12 @@ impl Parser {
) -> Option<&Schema> {
match complex.get("type") {
Some(Value::String(typ)) => {
+ // A `type` that is not a valid Avro name cannot match any
+ // already-seen schema, so treat it as "not seen" rather than
+ // panicking. The real validation error is surfaced later by
+ // the parse path that calls `Name::parse`.
let name =
- Name::new_with_enclosing_namespace(typ.as_str(),
enclosing_namespace).unwrap();
+ Name::new_with_enclosing_namespace(typ.as_str(),
enclosing_namespace).ok()?;
self.resolving_schemas
.get(&name)
.or_else(|| self.parsed_schemas.get(&name))
@@ -548,7 +552,7 @@ impl Parser {
let fully_qualified_name = Name::parse(complex, enclosing_namespace)?;
let aliases =
- self.fix_aliases_namespace(complex.aliases(),
fully_qualified_name.namespace());
+ self.fix_aliases_namespace(complex.aliases(),
fully_qualified_name.namespace())?;
let mut lookup = BTreeMap::new();
@@ -622,7 +626,7 @@ impl Parser {
let fully_qualified_name = Name::parse(complex, enclosing_namespace)?;
let aliases =
- self.fix_aliases_namespace(complex.aliases(),
fully_qualified_name.namespace());
+ self.fix_aliases_namespace(complex.aliases(),
fully_qualified_name.namespace())?;
let symbols: Vec<String> = symbols_opt
.and_then(|v| v.as_array())
@@ -771,7 +775,7 @@ impl Parser {
let fully_qualified_name = Name::parse(complex, enclosing_namespace)?;
let aliases =
- self.fix_aliases_namespace(complex.aliases(),
fully_qualified_name.namespace());
+ self.fix_aliases_namespace(complex.aliases(),
fully_qualified_name.namespace())?;
let schema = Schema::Fixed(FixedSchema {
name: fully_qualified_name.clone(),
@@ -795,32 +799,38 @@ impl Parser {
&self,
aliases: Option<Vec<String>>,
namespace: NamespaceRef,
- ) -> Aliases {
- aliases.map(|aliases| {
- aliases
- .iter()
- .map(|alias| {
- if alias.find('.').is_none() {
- match namespace {
- Some(ns) => format!("{ns}.{alias}"),
- None => alias.clone(),
+ ) -> AvroResult<Aliases> {
+ aliases
+ .map(|aliases| {
+ aliases
+ .iter()
+ .map(|alias| {
+ // An alias that is not a valid Avro name is a schema
+ // error — propagate it instead of unwrapping (which
+ // would panic on attacker-controlled schema JSON).
+ if alias.contains('.') {
+ Alias::new(alias.as_str())
+ } else {
+ match namespace {
+ Some(ns) =>
Alias::new(&format!("{ns}.{alias}")),
+ None => Alias::new(alias.as_str()),
+ }
}
- } else {
- alias.clone()
- }
- })
- .map(|alias| Alias::new(alias.as_str()).unwrap())
- .collect()
- })
+ })
+ .collect::<AvroResult<Vec<_>>>()
+ })
+ .transpose()
}
- fn get_schema_type_name(&self, name: Name, value: &Value) -> Name {
+ fn get_schema_type_name(&self, name: Name, value: &Value) ->
AvroResult<Name> {
match value.get("type") {
Some(Value::Object(complex_type)) => match complex_type.name() {
- Some(name) => Name::new(name).unwrap(),
- _ => name,
+ // Propagate the validation error if the nested `type` name is
+ // not a valid Avro name, rather than panicking on `unwrap()`.
+ Some(type_name) => Name::new(type_name),
+ _ => Ok(name),
},
- _ => name,
+ _ => Ok(name),
}
}
diff --git a/avro/tests/avro-rs-547.rs b/avro/tests/avro-rs-547.rs
new file mode 100644
index 0000000..7589dc2
--- /dev/null
+++ b/avro/tests/avro-rs-547.rs
@@ -0,0 +1,71 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Regression: parsing a schema whose name / alias / type-reference is not a
+//! valid Avro identifier must return `Err`, never panic.
+//!
+//! `Schema::parse_str` is a `Result`-returning, public API; callers cannot
+//! guard against an internal `unwrap()`, so a panic on attacker-controlled
+//! schema JSON is a denial-of-service. `Name::parse` was fixed previously,
+//! but the sibling sites in `schema::parser` (`get_already_seen_schema`,
+//! `fix_aliases_namespace`, `get_schema_type_name`) still called
+//! `Name::new(..).unwrap()` / `Alias::new(..).unwrap()`.
+
+use apache_avro::Schema;
+
+/// Each of these declares a name, alias, or type reference that is not a valid
+/// Avro identifier. Before the fix at least one site (`fix_aliases_namespace`)
+/// panicked with `called 'Result::unwrap()' on an 'Err' value: Invalid schema
+/// name ...`.
+const INVALID_NAME_SCHEMAS: &[&str] = &[
+ // invalid `aliases` entry on a record / fixed / enum
+ r#"{"type": "record", "name": "R", "aliases": [":"], "fields": []}"#,
+ r#"{"type": "fixed", "name": "F", "aliases": ["a.b:c"], "size": 4}"#,
+ r#"{"type": "enum", "name": "E", "aliases": [""], "symbols": ["A"]}"#,
+ // invalid nested type name
+ r#"{"type": "record", "name": "R", "fields": [
+ {"name": "f", "type": {"type": "record", "name": ":", "fields": []}}
+ ]}"#,
+ // invalid string type reference
+ r#"{"type": "record", "name": "R", "fields": [{"name": "f", "type":
":"}]}"#,
+ // invalid top-level name
+ r#"{"type": "record", "name": ":", "fields": []}"#,
+];
+
+#[test]
+fn invalid_schema_names_return_err_not_panic() {
+ for schema in INVALID_NAME_SCHEMAS {
+ let result = Schema::parse_str(schema);
+ assert!(
+ result.is_err(),
+ "expected Err (not a panic) for invalid-name schema:\n{schema}"
+ );
+ }
+}
+
+#[test]
+fn valid_schema_with_aliases_still_parses() {
+ // Guards against the fix over-rejecting: legitimate aliases must still
work.
+ let schema = r#"{
+ "type": "record",
+ "name": "R",
+ "namespace": "ns",
+ "aliases": ["Old", "other.Older"],
+ "fields": [{"name": "f", "type": "int"}]
+ }"#;
+ assert!(Schema::parse_str(schema).is_ok());
+}