Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/vector_graphics_compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## NEXT
## 1.2.4

* Fix Stack Overflow crashes caused by circular references (masks, patterns, deferred nodes, and clip paths).
* Prevent CPU/Memory Denial of Service (DoS) resource exhaustion from exponential DAG reference expansions (Billion Laughs SVG exploits) by enforcing a strict, cumulative reference expansion safety limit of 1,000.
* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10.

## 1.2.3
Expand Down
10 changes: 10 additions & 0 deletions packages/vector_graphics_compiler/lib/src/svg/constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// The maximum number of nested reference expansions allowed in an SVG to prevent DoS exploits.
const int kMaxReferenceExpansions = 1000;
Comment thread
jeffkwoh marked this conversation as resolved.

Comment thread
jeffkwoh marked this conversation as resolved.
/// The error message thrown when the nested reference expansions limit is exceeded.
const String kMaxReferenceExpansionsErrorMessage =
'SVG contains too many nested reference expansions (possible Denial of Service exploit).';
17 changes: 16 additions & 1 deletion packages/vector_graphics_compiler/lib/src/svg/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import '../vector_instructions.dart';
import 'clipping_optimizer.dart';
import 'color_mapper.dart';
import 'colors.dart';
import 'constants.dart';
import 'masking_optimizer.dart';
import 'node.dart';
import 'numbers.dart' as numbers show parseDoubleWithUnits;
Expand Down Expand Up @@ -1678,6 +1679,7 @@ class _Resolver {
final Map<String, AttributedNode> _drawables = <String, AttributedNode>{};
final Map<String, Gradient> _shaders = <String, Gradient>{};
final Map<String, List<Node>> _clips = <String, List<Node>>{};
int _deferredExpansionCount = 0;

bool _sealed = false;

Expand All @@ -1702,6 +1704,7 @@ class _Resolver {

final pathBuilders = <PathBuilder>[];
PathBuilder? currentPath;
final activeDeferred = <String>{};
void extractPathsFromNode(Node? target) {
if (target is PathNode) {
final nextPath = PathBuilder.fromPath(target.path);
Expand All @@ -1716,7 +1719,19 @@ class _Resolver {
currentPath!.addPath(nextPath.toPath(reset: false));
}
} else if (target is DeferredNode) {
extractPathsFromNode(target.resolver(target.refId));
_deferredExpansionCount++;
if (_deferredExpansionCount > kMaxReferenceExpansions) {
throw StateError(kMaxReferenceExpansionsErrorMessage);
}
if (!activeDeferred.add(target.refId)) {
// Recursive loop detected.
return;
}
try {
extractPathsFromNode(target.resolver(target.refId));
} finally {
activeDeferred.remove(target.refId);
}
} else if (target is ParentNode) {
target.visitChildren(extractPathsFromNode);
}
Expand Down
118 changes: 80 additions & 38 deletions packages/vector_graphics_compiler/lib/src/svg/resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import '../geometry/path.dart';
import '../geometry/vertices.dart';
import '../image/image_info.dart';
import '../paint.dart';
import 'constants.dart';
import 'node.dart';
import 'parser.dart';
import 'visitor.dart';
Expand All @@ -19,6 +20,11 @@ import 'visitor.dart';
class ResolvingVisitor extends Visitor<Node, AffineMatrix> {
late Rect _bounds;

final Set<String> _activeMasks = <String>{};
final Set<String> _activeDeferred = <String>{};
final Set<String> _activePatterns = <String>{};
int _deferredExpansionCount = 0;

@override
Node visitClipNode(ClipNode clipNode, AffineMatrix data) {
final AffineMatrix childTransform = clipNode.concatTransform(data);
Expand All @@ -37,19 +43,31 @@ class ResolvingVisitor extends Visitor<Node, AffineMatrix> {

@override
Node visitMaskNode(MaskNode maskNode, AffineMatrix data) {
final AttributedNode? resolvedMask = maskNode.resolver(maskNode.maskId);
if (resolvedMask == null) {
_deferredExpansionCount++;
if (_deferredExpansionCount > kMaxReferenceExpansions) {
throw StateError(kMaxReferenceExpansionsErrorMessage);
}
if (!_activeMasks.add(maskNode.maskId)) {
// Recursive loop detected.
return maskNode.child.accept(this, data);
}
Comment thread
jeffkwoh marked this conversation as resolved.
final Node child = maskNode.child.accept(this, data);
final AffineMatrix childTransform = maskNode.concatTransform(data);
final Node mask = resolvedMask.accept(this, childTransform);

return ResolvedMaskNode(
child: child,
mask: mask,
blendMode: maskNode.blendMode,
);
try {
final AttributedNode? resolvedMask = maskNode.resolver(maskNode.maskId);
if (resolvedMask == null) {
return maskNode.child.accept(this, data);
}
final Node child = maskNode.child.accept(this, data);
final AffineMatrix childTransform = maskNode.concatTransform(data);
final Node mask = resolvedMask.accept(this, childTransform);

return ResolvedMaskNode(
child: child,
mask: mask,
blendMode: maskNode.blendMode,
);
} finally {
_activeMasks.remove(maskNode.maskId);
}
}

@override
Expand Down Expand Up @@ -180,17 +198,29 @@ class ResolvingVisitor extends Visitor<Node, AffineMatrix> {

@override
Node visitDeferredNode(DeferredNode deferredNode, AffineMatrix data) {
final AttributedNode? resolvedNode = deferredNode.resolver(
deferredNode.refId,
);
if (resolvedNode == null) {
_deferredExpansionCount++;
if (_deferredExpansionCount > kMaxReferenceExpansions) {
throw StateError(kMaxReferenceExpansionsErrorMessage);
}
if (!_activeDeferred.add(deferredNode.refId)) {
// Recursive loop detected.
return Node.empty;
}
final Node concreteRef = resolvedNode.applyAttributes(
deferredNode.attributes,
replace: true,
);
return concreteRef.accept(this, data);
try {
final AttributedNode? resolvedNode = deferredNode.resolver(
deferredNode.refId,
);
if (resolvedNode == null) {
return Node.empty;
}
final Node concreteRef = resolvedNode.applyAttributes(
deferredNode.attributes,
replace: true,
);
return concreteRef.accept(this, data);
} finally {
_activeDeferred.remove(deferredNode.refId);
}
}

@override
Expand Down Expand Up @@ -293,26 +323,38 @@ class ResolvingVisitor extends Visitor<Node, AffineMatrix> {

@override
Node visitPatternNode(PatternNode patternNode, AffineMatrix data) {
final AttributedNode? resolvedPattern = patternNode.resolver(
patternNode.patternId,
);
if (resolvedPattern == null) {
_deferredExpansionCount++;
if (_deferredExpansionCount > kMaxReferenceExpansions) {
throw StateError(kMaxReferenceExpansionsErrorMessage);
}
if (!_activePatterns.add(patternNode.patternId)) {
// Recursive loop detected.
return patternNode.child.accept(this, data);
}
Comment thread
jeffkwoh marked this conversation as resolved.
final Node child = patternNode.child.accept(this, data);
final AffineMatrix childTransform = patternNode.concatTransform(data);
final Node pattern = resolvedPattern.accept(this, childTransform);

return ResolvedPatternNode(
child: child,
pattern: pattern,
x: resolvedPattern.attributes.x?.calculate(0) ?? 0,
y: resolvedPattern.attributes.y?.calculate(0) ?? 0,
width: resolvedPattern.attributes.width!,
height: resolvedPattern.attributes.height!,
transform: data,
id: patternNode.patternId,
);
try {
final AttributedNode? resolvedPattern = patternNode.resolver(
patternNode.patternId,
);
if (resolvedPattern == null) {
return patternNode.child.accept(this, data);
}
final Node child = patternNode.child.accept(this, data);
final AffineMatrix childTransform = patternNode.concatTransform(data);
final Node pattern = resolvedPattern.accept(this, childTransform);

return ResolvedPatternNode(
child: child,
pattern: pattern,
x: resolvedPattern.attributes.x?.calculate(0) ?? 0,
y: resolvedPattern.attributes.y?.calculate(0) ?? 0,
width: resolvedPattern.attributes.width!,
height: resolvedPattern.attributes.height!,
transform: data,
id: patternNode.patternId,
);
} finally {
_activePatterns.remove(patternNode.patternId);
}
}

@override
Expand Down
2 changes: 1 addition & 1 deletion packages/vector_graphics_compiler/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: vector_graphics_compiler
description: A compiler to convert SVGs to the binary format used by `package:vector_graphics`.
repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics_compiler
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22
version: 1.2.3
version: 1.2.4

executables:
vector_graphics_compiler:
Expand Down
Loading
Loading