Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix various security vulnerabilites found by libFuzzer, part 2 #6418

Merged
merged 11 commits into from
Oct 7, 2021
Merged

Fix various security vulnerabilites found by libFuzzer, part 2 #6418

merged 11 commits into from
Oct 7, 2021

Conversation

MaxKellermann
Copy link
Contributor

This PR starts with a few build breakage fixes which occur when almost all optional features are disabled at compile time.
The last 3 commits fix security vulnerabilities I found with libFuzzer, one of which qualifies for remote code execution, the worst kind of vulnerability.
A crash report from Google Play of an open source app using a copy of MapServer code (https://github.com/XCSoar/XCSoar/) contained a crash due to one of these vulnerabilities, so I investigated and turned on libFuzzer again, which quickly found more vulnerabilities.
I used this program (specific to my app's internal APIs wrapping our my copy of the MapServer code) to run libFuzzer: https://github.com/XCSoar/XCSoar/blob/master/fuzzer/src/FuzzTopographyFile.cpp

This is similar to my previous PR earlier this year: #6319
Back then, @rouault asked me to change the PR to the "main" branch; do you want me to do the same here? (My way of managing my open source apps is that I always merge bug fixes to the "stable" branch first, and then merge all "stable" bug fixes into the "main" branch.)

@rouault
Copy link
Contributor

rouault commented Oct 5, 2021

do you want me to do the same here?

at least, issue also a corresponding PR against master. Note also that 7.6 branch will receive little maintenance at that point

@jmckenna
Copy link
Member

jmckenna commented Oct 5, 2021

wow thanks @MaxKellermann. This will spawn us to have another 7.6..x release with this security fix. Great find.

mapserv.c Outdated Show resolved Hide resolved
@rouault
Copy link
Contributor

rouault commented Oct 5, 2021

This will spawn us to have another 7.6..x release with this security fix. Great find.

as far as I can tell, the vulnerability is if you read from corrupted / hostile shapefiles. It isn't something that is triggered by remote requests, so the severity is much less important

The canonical macro is _WIN32.  WIN32 usually exists as well, but is a
non-standard macro.

See https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros?view=msvc-160
Fixes various -Wunused warnings.
With some compile-time options, pointObj has less than 4 fields, so
this patch also fixes the build with those options.
Traverse the string only once.  Also, this removes code which
triggered -Wsign-compare.
The data in panParts is never checked.  The only check was
"numpoints<=0", but that is not enough.

Three very bad things can happen:

- arbitrary huge values, leading to allocations of up to two billion
  elements (INT_MAX), bypassing the 50 million limit which was
  previously put on "nPoints"

- overflowing the "pabyRec" buffer in the memcpy() call

- integer overflow in the malloc() call, writing past the allocated
  buffer

The latter is probably enough for remote code execution.

Vulnerability found with libFuzzer.
After freeing the "line" field, we need to clear it, or else it will
be freed again in msFreeShape().

In two code paths, the "numlines" field was not cleared, which could
lead to a use-after-free bug in msFreeShape(), which in turn could
either crash or lead to another double-free bug in msFreeShape().

Vulnerability found with libFuzzer.
With a crafted shapefile, it was possible to put
msShapefileWhichShapes() into an extremely long loop, calling
msSHPReadBounds() over and over, even if all of those calls fail.

This patch adds error checking, and if an error occurs,
msShapefileWhichShapes() gives up, because after an I/O error, there
is no reasonable chance that anything will ever work properly.

Vulnerability found by libFuzzer.
@MaxKellermann
Copy link
Contributor Author

as far as I can tell, the vulnerability is if you read from corrupted / hostile shapefiles.

Correct.

@jmckenna
Copy link
Member

jmckenna commented Oct 5, 2021

Unfortunately shapefiles are still widely used by MapServer users, it's still an important fix and I think should warrant a security release.

@MaxKellermann
Copy link
Contributor Author

it's still an important fix and I think should warrant a security release.

Agree. These are severe vulnerabilities.
They may not be directly exploitable through the MapServer API, but once you find a way to convince a MapServer to open your shapefile, you own the server.

Just because we can't currently imagine a way to exploit a server remotely doesn't mean that somebody with more creativity can't find a way. Better be too careful than sorry.

The coding style in this project is very fragile towards such vulnerabilities. For example, the code checking buffer sizes looks fragile and obscure; stuff like:

MapServer/mapshape.c

Lines 770 to 773 in 1c14821

for( i = 0; i < shape->numlines; i++ ) {
for( j = 0; j < shape->line[i].numpoints; j++ ) {
ByteCopy( &(shape->line[i].point[j].x), pabyRec + 44 + 4*t_nParts + 8 + k * 16, 8 );
ByteCopy( &(shape->line[i].point[j].y), pabyRec + 44 + 4*t_nParts + 8 + k * 16 + 8, 8 );

So many magic numbers! Where do they come from? Near that code fragment, I don't see any buffer size verification. Maybe there is verification somewhere else, but it is not obvious at all that this code is correct; quite contrary, you made it extremely difficult for a human to verify this piece of code.

Much of the code deals with 32 bit signed integers for sizes. That's why you always need an additional <0 check. Just use unsigned integers! And watch out for integer truncation when you cast size_t to int.

What about integer overflows?

MapServer/mapshape.c

Lines 617 to 620 in 1c14821

psSHP->nMaxRecords = (int) (psSHP->nMaxRecords * 1.3 + 100);
psSHP->panRecOffset = (int *) SfRealloc(psSHP->panRecOffset,sizeof(int) * psSHP->nMaxRecords );
psSHP->panRecSize = (int *) SfRealloc(psSHP->panRecSize,sizeof(int) * psSHP->nMaxRecords );

This will easily overflow, leading to a SfRealloc() call with a negative integer. Oh yes, SfRealloc() takes a signed integer, WTF. What happens with this negative integer when it's casted back to size_t for realloc()?

Maybe you could argue that this and that limit in the code prevents this integer overflow. Maybe, maybe somebody finds a way around this. My point is: the MapServer code is horribly fragile, it's the anti-style for making secure code. Everywhere I look, I see potential for vulnerabilities.

Of course, it's plain C, and getting error code paths right in plain C is utterly difficult. Just look at the many memory leaks, or worse, the double-free bugs I found (leaks are just DoS, but double-free often leads to remote code execution). C is simply the wrong tool for the job. You could easily improve the code quality by switching all sources to a C++ compiler and use RAII to manage allocations. (Some argue that C++ isn't safe enough and everybody should be using Rust, but I'm a C++ fanboy, and C++ is certainly a great improvement over plain C, and switching is mostly trivial.)

@sdlime
Copy link
Member

sdlime commented Oct 5, 2021

@MaxKellermann, your candor is appreciated. Can you elaborate on "mostly trivial"?

@MaxKellermann
Copy link
Contributor Author

@MaxKellermann, your candor is appreciated. Can you elaborate on "mostly trivial"?

More like commit 098eb92 (by @rouault). That is: rename *.c to *.cpp, which just switches from the C compiler to the C++ compiler. This usually needs a few adjustments; C++ is mostly a superset of C, but there are small differences.

The whole int -> bool transition was good, but has nothing to do with the C++ switch (bool has been a plain C feature for 22 years already, and using int as boolean has been obsolete ever since); this just makes the transition look more complicated than it needs to be. Most of the other changes in 098eb92 were unnecessary, too. I would really just touch the few things that are really different in C/C++ in the very first step.

Second step (optional): small syntactic stuff like NULL -> nullptr.

Third step (the interesting part): switch to safe C++ data structures, use RAII to free resources automatically, wrap legacy code / thirdparty C libraries in safe C++ wrappers. This eliminates all of the ugly cleanup code like:

MapServer/mapshape.c

Lines 1354 to 1364 in 1c14821

if (shape->line[i].numpoints <= 0 || shape->line[i].numpoints > nPoints) {
msSetError(MS_SHPERR, "Corrupted .shp file : shape %d, shape->line[%d].numpoints=%d", "msSHPReadShape()",
hEntity, i, shape->line[i].numpoints);
while(--i >= 0)
free(shape->line[i].point);
free(shape->line);
shape->line = NULL;
shape->numlines = 0;
shape->type = MS_SHAPE_NULL;
return;
}

... which is very fragile, because it's easy to forget something, and if you add a new struct field, you have to adjust all error code paths.

Fourth step: migrate the error handling to C++ exceptions.

@MaxKellermann MaxKellermann mentioned this pull request Oct 5, 2021
@MaxKellermann
Copy link
Contributor Author

Added a backport of 8dc4104 and a fix for #6421 (comment)

@rouault rouault merged commit d5eaaa0 into MapServer:branch-7-6 Oct 7, 2021
@MaxKellermann MaxKellermann deleted the fuzzer2 branch October 15, 2021 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants