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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ body:
description: Please input any other notes that may be relevant, e.g. do you have any thoughts on what might be wrong?
placeholder: ...
validations:
required: false
required: false
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
blank_issues_enabled: true
blank_issues_enabled: true
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/newfeature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ body:
description: Would you be interested to contribute code that adds this functionality through a Pull Request? We gladly accept PRs - it's much faster and you'll be added a contributor.
options:
- label: I am interested in implementing the described feature request and submit as a PR.
required: false
required: false
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/question.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ body:
description: Please describe what you would like to know about PyShp, e.g. how to do something.
placeholder: ...
validations:
required: true
required: true
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/unexpected.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ body:
description: Please input any other notes that may be relevant, e.g. do you have any thoughts on what might be wrong?
placeholder: ...
validations:
required: false
required: false
2 changes: 1 addition & 1 deletion .github/actions/build_wheel_and_sdist/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ runs:
uses: actions/upload-artifact@v7
with:
name: PyShp_wheel_and_sdist
path: dist
path: dist
1 change: 0 additions & 1 deletion .github/workflows/run_checks_build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,3 @@ jobs:
replace_remote_urls_with_localhost: ${{ !(matrix.os == 'ubuntu-latest' && matrix.python-version == '3.15') }}
# Checkout to ./PyShp, as the test job also needs to check out the artefact repo
pyshp_repo_directory: ./Pyshp

3 changes: 0 additions & 3 deletions .github/workflows/speed_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,3 @@ jobs:
env:
PYSHP_TEST_REPO: ./PyShp_test_shapefile
run: python ./Pyshp/tests/run_benchmarks.py



22 changes: 17 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,33 @@ repos:
entry: uv run ruff check
language: system
pass_filenames: false

- id: ruff-format
name: ruff format
entry: uv run ruff format
language: system
pass_filenames: false

- id: mypy
name: mypy
entry: uv run mypy --strict .
language: system
pass_filenames: false

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-yaml
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
name: trim trailing whitespace
entry: trailing-whitespace-fixer
language: system
types: [text]

- id: end-of-file-fixer
name: fix end of files
entry: end-of-file-fixer
language: system
types: [text]

- id: check-yaml
name: check yaml
entry: check-yaml
language: system
types: [yaml]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1888,4 +1888,4 @@ Users of the previous version 1.x should therefore take note of the following ch
- Fixed bug when writing shapefiles with datefield and date values earlier than 1900 [@megies]
- Fix some geo interface errors, including checking polygon directions.
- Bug fixes for reading from case sensitive file names, individual files separately, and from file-like objects. [@gastoneb, @kb003308, @erickskb]
- Enforce maximum field limit. [@mwtoews]
- Enforce maximum field limit. [@mwtoews]
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ There is no bug bounty. But one maintainer has bought people a drink before, fo
- PyShp is a pure-Python library that typically gets called from other Python code. Attackers with the ability to run Python code on a target in the first place, can already peform any function Python allows them to. As long as PyShp was not the means by which the Attacker gained the ability to run arbitrary Python code, PyShp cannot prevent this.
- `eval`, `compile` and `exec` etc. are not used, and should be considered to be banned.
- The vulnerabilities we fail to consider are even more important than those we are aware of. That said, by its very nature, PyShp does read arbitrary binary data. An attack via a maliciously crafted shapefile is possible. To mitigate this, PyShp -should- only convert arbitrary binary data into specific Python types (floats, ints, strings, booleans, dates, lists, arrays, tuples and dicts). These do contain arbitrary data, but they should never result in arbitrary code execution (or even in Python raising a non-standard system-affecting Exception or crash).
- We therefore consider the overall security risk from using PyShp to be low. If you think otherwise, please contact a maintainer.
- We therefore consider the overall security risk from using PyShp to be low. If you think otherwise, please contact a maintainer.
3 changes: 0 additions & 3 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,3 @@ VERSION 1.1.7
*README.txt (main) Added tests for iterShapes(), iterRecords(), __geo_interface__()

*README.txt (main) Updated "bbox" property documentation to match Esri specification.



10 changes: 7 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,15 @@ lint = [
"mypy==1.19.1", # last to support Python 3.9
'pre-commit; python_version >= "3.10"',
"ruff",
"pre-commit-hooks",
]
test = [
"pytest", "hypothesis"
"pytest", "hypothesis", "pytest-timeout"
]

[tool.uv]
exclude-dependencies = [
{ package = { name = "pre-commit" }, dependencies = ["virtualenv"] },
]


Expand Down Expand Up @@ -144,5 +150,3 @@ skip-magic-trailing-comma = false

# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"


27 changes: 18 additions & 9 deletions src/shapefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2834,6 +2834,10 @@ class ShapefileException(Exception):
"""An exception to handle shapefile specific problems."""


class CorruptShpShapeRecord(ShapefileException):
"""Indicates invalid metadata in Shape record headers."""


class dbfFileException(ShapefileException):
"""Indicates a problem with the .dbf file."""

Expand Down Expand Up @@ -3402,7 +3406,11 @@ def _headers_cached_gen(self) -> Iterator[ShapeHeaderInfoT]:
self.file.seek(header_pos)
# Unpack the shape header only
recNum, recLength_16bw = unpack(">2i", self.file.read(8))

if recLength_16bw <= 0:
raise CorruptShpShapeRecord(
f"At offset: {header_pos} in .shp file got claimed record num: {recNum}"
f" and non-positive shape record length: {recLength_16bw} (16 bit words)"
)
shp_length_B = 2 * recLength_16bw
shape_header_data = header_pos, shp_length_B, recNum
self.headers_cache.append(shape_header_data)
Expand Down Expand Up @@ -3446,7 +3454,7 @@ def shape(
"""
if offset is None or shape_len_B is None:
offset, shape_len_B, _recNum = self.shape_header(index)
# Seek to 8B after the offset (past the shape's 8B header).
# Seek to 8B after the offset (past the shape's 8 byte header).
self.file.seek(offset + 8)
shape = self._shape(shape_len_B=shape_len_B, oid=index, bbox=bbox)
return shape
Expand Down Expand Up @@ -3476,6 +3484,12 @@ def _shape(
except StructError as e:
raise e

if shapeType not in (NULL, self.shapeType):
raise CorruptShpShapeRecord(
f"Shape type code in .shp file header: {self.shapeType} does not match "
f"the non-null shape type code in the shape record: {shapeType} (oid: {oid!r})"
)

ShapeClass = SHAPE_CLASS_FROM_SHAPETYPE[shapeType]
shape = ShapeClass.from_byte_stream(
shapeType=shapeType,
Expand All @@ -3485,11 +3499,6 @@ def _shape(
bbox=bbox,
)

# Seek to the end of this record as defined by the record header because
# the shapefile spec doesn't require the actual content to meet the header
# definition. Probably allowed for lazy feature deletion.
# self.file.seek(next_shape_pos)

return shape

def shapes(
Expand Down Expand Up @@ -4767,8 +4776,8 @@ def _shp_record(self, s: Shape) -> tuple[int, int]:
self.shapeType = s.shapeType
if s.shapeType not in (NULL, self.shapeType):
raise ShapefileException(
f"The shape's type ({s.shapeType}) must match "
f"the type of the shapefile ({self.shapeType})."
f"The shape's type: {s.shapeType} must be Null, or match "
f"the type of the shapefile: {self.shapeType}."
)

# For both single point and multiple-points non-null shapes,
Expand Down
2 changes: 1 addition & 1 deletion tests/run_benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,4 @@ def main():
run(1, writer_benchmarks.values()) # type: ignore [arg-type]

if __name__ == "__main__":
main()
main()
27 changes: 23 additions & 4 deletions tests/test_shapefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import os.path
from pathlib import Path
import shutil
import struct

# third party imports
# third party deps
import pytest

# our imports (the code under test)
# The code under test
import shapefile

# helper functions
Expand Down Expand Up @@ -1353,6 +1354,26 @@ def test_reader_corrupt_files(tmp_path):
end = sf.shp.tell()
assert (end - stopped) == 5

@pytest.mark.timeout(10)
@pytest.mark.parametrize("corrupt_shape_length_16bw", [-4, -1])
def test_reader_corrupt_shape_record_header(corrupt_shape_length_16bw: int):
"""
Assert that reader raises on invalid shape record metadata.
"""
shp_file = shapefiles_dir / "blockgroups.shp"
stream = io.BytesIO(shp_file.read_bytes())
# Initial file header is 100B.
# 4B more into Shape record header is its record length.
stream.seek(0)
stream.seek(104)
stream.write(struct.pack(">i",corrupt_shape_length_16bw))
stream.seek(0)
with pytest.raises(shapefile.CorruptShpShapeRecord):
with shapefile.Reader(shp=stream) as reader:
reader.shape(i=-1)




def test_bboxfilter_shape():
"""
Expand Down Expand Up @@ -2209,5 +2230,3 @@ def test_read_latin1_shapefile_cpg_file(encoding, context, tmp_latin1_shapefile_
r.close()
if encoding == "latin1":
assert rec == [2, u'Ñandú']


Loading
Loading