aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsaturneric <[email protected]>2026-09-12 23:46:25 +0200
committersaturneric <[email protected]>2026-09-12 23:46:25 +0200
commitd688de18b5b25b4edc73fc8d3ba9511733c78a79 (patch)
tree258c677d38b4296f54325d9c987be7f8a57f3a4f
parentfix(imap): prevent Windows TEXT macro collision (diff)
downloadvmime-master.tar.gz
vmime-master.zip
fix(parsing): limit MIME nesting depthHEADmaster
- bound recursive multipart parsing to prevent stack exhaustion - preserve part bounds without descending past the configured limit - apply the limit to malformed-message recovery paths - expose configurable depth limits through parsingContext - preserve the limit when copying parsing contexts
-rw-r--r--src/vmime/body.cpp54
-rw-r--r--src/vmime/parsingContext.cpp15
-rw-r--r--src/vmime/parsingContext.hpp25
3 files changed, 88 insertions, 6 deletions
diff --git a/src/vmime/body.cpp b/src/vmime/body.cpp
index 0ef365e3..d644a6a7 100644
--- a/src/vmime/body.cpp
+++ b/src/vmime/body.cpp
@@ -121,6 +121,27 @@ size_t body::findNextBoundaryPosition(
}
+namespace {
+
+/** Nesting depth of a part, counted by walking to the root.
+ *
+ * A multipart body is parsed by recursion, so this is also the current stack
+ * depth. Bounded by the caller's limit, which is what makes the walk cheap.
+ */
+size_t nestingDepthOf(const bodyPart* part) {
+
+ size_t depth = 0;
+
+ for (const bodyPart* p = part; p != NULL; p = p->getParentPart()) {
+ ++depth;
+ }
+
+ return depth;
+}
+
+} // unnamed namespace
+
+
void body::parseImpl(
parsingContext& ctx,
const shared_ptr <utility::parserInputStreamAdapter>& parser,
@@ -131,6 +152,8 @@ void body::parseImpl(
removeAllParts();
+ const size_t maxDepth = ctx.getMaxNestingDepth();
+
m_prologText.clear();
m_epilogText.clear();
@@ -315,7 +338,19 @@ void body::parseImpl(
std::swap(partStart, partEnd);
}
- part->parse(ctx, parser, partStart, partEnd, NULL);
+ // Depth is stack depth here: descending without a bound lets a
+ // small crafted message exhaust the stack and kill the process,
+ // and a message must be parsed before anything can say whether
+ // it is trustworthy. Over the limit the part is kept, with its
+ // bounds, but not descended into.
+ if (maxDepth != 0 && nestingDepthOf(m_part) >= maxDepth) {
+
+ part->getBody()->setParsedBounds(partStart, partEnd);
+
+ } else {
+
+ part->parse(ctx, parser, partStart, partEnd, NULL);
+ }
m_parts.push_back(part);
}
@@ -335,10 +370,19 @@ void body::parseImpl(
shared_ptr <bodyPart> part = m_part->createChildPart();
- try {
- part->parse(ctx, parser, partStart, end);
- } catch (std::exception&) {
- throw;
+ // See above: the same bound applies to the recovery path, which is
+ // the one a malformed message is most likely to take.
+ if (maxDepth != 0 && nestingDepthOf(m_part) >= maxDepth) {
+
+ part->getBody()->setParsedBounds(partStart, end);
+
+ } else {
+
+ try {
+ part->parse(ctx, parser, partStart, end);
+ } catch (std::exception&) {
+ throw;
+ }
}
m_parts.push_back(part);
diff --git a/src/vmime/parsingContext.cpp b/src/vmime/parsingContext.cpp
index e80eab78..8c1272d8 100644
--- a/src/vmime/parsingContext.cpp
+++ b/src/vmime/parsingContext.cpp
@@ -35,7 +35,8 @@ parsingContext::parsingContext()
parsingContext::parsingContext(const parsingContext& ctx)
: context(ctx),
- m_headerParseErrorRecovery(vmime::headerParseRecoveryMethod::SKIP_LINE) {
+ m_headerParseErrorRecovery(vmime::headerParseRecoveryMethod::SKIP_LINE),
+ m_maxNestingDepth(ctx.m_maxNestingDepth) {
}
@@ -79,6 +80,18 @@ bool parsingContext::getUseMyHostname() const {
}
+size_t parsingContext::getMaxNestingDepth() const {
+
+ return m_maxNestingDepth;
+}
+
+
+void parsingContext::setMaxNestingDepth(const size_t depth) {
+
+ m_maxNestingDepth = depth;
+}
+
+
void parsingContext::setUseMyHostname(bool useMyHostname) {
m_useMyHostname = useMyHostname;
diff --git a/src/vmime/parsingContext.hpp b/src/vmime/parsingContext.hpp
index 95c51959..817fffa9 100644
--- a/src/vmime/parsingContext.hpp
+++ b/src/vmime/parsingContext.hpp
@@ -101,6 +101,26 @@ public:
*/
void setUseMyHostname(bool useMyHostname);
+ /** Returns the maximum MIME nesting depth this context will parse.
+ *
+ * A multipart body is parsed by recursion, so nesting depth is stack
+ * depth: a message is a few hundred bytes per level, and a few thousand
+ * levels exhausts the stack and terminates the process. Since a message
+ * has to be parsed before anything can say whether it is trustworthy,
+ * that is reachable from unauthenticated input.
+ *
+ * Parts deeper than this are left unparsed rather than descended into.
+ *
+ * @return the maximum nesting depth, or 0 for unlimited
+ */
+ size_t getMaxNestingDepth() const;
+
+ /** Sets the maximum MIME nesting depth. See getMaxNestingDepth.
+ *
+ * @param depth maximum depth, or 0 for unlimited
+ */
+ void setMaxNestingDepth(const size_t depth);
+
protected:
headerParseRecoveryMethod::headerLineError m_headerParseErrorRecovery;
@@ -120,6 +140,11 @@ protected:
* for header fields when one is not present.
*/
bool m_useMyHostname{true};
+
+ /** Maximum MIME nesting depth; 0 means unlimited. See
+ * getMaxNestingDepth().
+ */
+ size_t m_maxNestingDepth{1024};
};