|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | # vim: set et ts=8 sts=4 sw=4 ai: |
3 | 3 |
|
| 4 | +import os |
4 | 5 | import pytest |
5 | 6 | from otterwiki.renderer import render |
6 | 7 | from bs4 import BeautifulSoup |
@@ -890,6 +891,84 @@ def test_datatable_csv_absolute_src(create_app): |
890 | 891 | assert "City" in html |
891 | 892 |
|
892 | 893 |
|
| 894 | +def _write_secret_outside_repo(create_app): |
| 895 | + """Create a secret file in the parent directory of the repository and |
| 896 | + return (path, content). Attachments live in `<repo>/<page>/`, so a |
| 897 | + `../../` traversal from there lands in the repository's parent.""" |
| 898 | + secret_content = "SECRET_KEY = super-secret-value-12345\n" |
| 899 | + secret_path = os.path.join( |
| 900 | + os.path.dirname(create_app.storage.path.rstrip(os.sep)), |
| 901 | + "secret.cfg", |
| 902 | + ) |
| 903 | + with open(secret_path, "w") as f: |
| 904 | + f.write(secret_content) |
| 905 | + return secret_path, secret_content |
| 906 | + |
| 907 | + |
| 908 | +def test_datatable_csv_path_traversal_relative_blocked(create_app): |
| 909 | + """A relative src with `..` must not read files outside the repository.""" |
| 910 | + secret_path, secret_content = _write_secret_outside_repo(create_app) |
| 911 | + try: |
| 912 | + author = ("Test Author", "test@example.com") |
| 913 | + # a genuine attachment so the page's attachment directory exists |
| 914 | + create_app.storage.store( |
| 915 | + "csvtrav.md", |
| 916 | + content="# Traversal\n{{datatable\n|src=../../secret.cfg\n" |
| 917 | + "|header=false\n}}\n", |
| 918 | + author=author, |
| 919 | + message="traversal page", |
| 920 | + ) |
| 921 | + create_app.storage.store( |
| 922 | + "csvtrav/data.csv", |
| 923 | + content="a;b\n1;2\n", |
| 924 | + author=author, |
| 925 | + message="add csv", |
| 926 | + ) |
| 927 | + client = create_app.test_client() |
| 928 | + response = client.get("/Csvtrav/view") |
| 929 | + assert response.status_code == 200 |
| 930 | + html = response.data.decode() |
| 931 | + # the secret must not leak into the rendered page |
| 932 | + assert "super-secret-value-12345" not in html |
| 933 | + # and the embedding reports a clear error instead |
| 934 | + assert "traversal" in html.lower() |
| 935 | + finally: |
| 936 | + os.remove(secret_path) |
| 937 | + |
| 938 | + |
| 939 | +def test_datatable_csv_path_traversal_absolute_blocked(create_app): |
| 940 | + """An absolute src with `..` must not escape the repository either.""" |
| 941 | + secret_path, secret_content = _write_secret_outside_repo(create_app) |
| 942 | + try: |
| 943 | + author = ("Test Author", "test@example.com") |
| 944 | + create_app.storage.store( |
| 945 | + "csvtravabs.md", |
| 946 | + content="# Traversal\n{{datatable\n" |
| 947 | + "|src=/csvtravabs/../../../secret.cfg\n|header=false\n}}\n", |
| 948 | + author=author, |
| 949 | + message="traversal abs page", |
| 950 | + ) |
| 951 | + client = create_app.test_client() |
| 952 | + response = client.get("/Csvtravabs/view") |
| 953 | + assert response.status_code == 200 |
| 954 | + html = response.data.decode() |
| 955 | + assert "super-secret-value-12345" not in html |
| 956 | + assert "traversal" in html.lower() |
| 957 | + finally: |
| 958 | + os.remove(secret_path) |
| 959 | + |
| 960 | + |
| 961 | +def test_attachment_rejects_path_traversal(create_app): |
| 962 | + """The Attachment constructor guards against traversal for every caller, |
| 963 | + not just the DataTable embedding.""" |
| 964 | + from otterwiki.gitstorage import StorageError |
| 965 | + from otterwiki.wiki import Attachment |
| 966 | + |
| 967 | + with create_app.app_context(): |
| 968 | + with pytest.raises(StorageError): |
| 969 | + Attachment("somepage", "../../secret.cfg") |
| 970 | + |
| 971 | + |
893 | 972 | def test_attachmentlist(create_app): |
894 | 973 | author = ("Test Author", "test@example.com") |
895 | 974 | create_app.storage.store( |
|
0 commit comments