Compare commits
No commits in common. "b1a7c8c3257cc87d0ad9d3be11f17d0b03421414" and "4408bbfc82d0088a20fab74b40c629e432b159ad" have entirely different histories.
b1a7c8c325
...
4408bbfc82
@ -2,20 +2,6 @@ Change Log
|
||||
==========
|
||||
|
||||
|
||||
Version 1.5.4
|
||||
-------------
|
||||
|
||||
Released 2023/5/17
|
||||
|
||||
Security fixes.
|
||||
|
||||
* Added safeguard to the next URI utilities, to prevent Cross-Site
|
||||
Scripting (XSS) attacks.
|
||||
* Applied the safe next URI utilities to the test site.
|
||||
* Added the ``SameSite`` and ``Secure`` flags to the session cookie
|
||||
of the test site.
|
||||
|
||||
|
||||
Version 1.5.3
|
||||
-------------
|
||||
|
||||
|
@ -24,7 +24,7 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from accounting.utils.user import UserUtilityInterface
|
||||
|
||||
VERSION: str = "1.5.4"
|
||||
VERSION: str = "1.5.3"
|
||||
"""The package version."""
|
||||
db: SQLAlchemy = SQLAlchemy()
|
||||
"""The database instance."""
|
||||
|
@ -32,7 +32,7 @@ First written: 2023/1/30
|
||||
{{ A_("New") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||
<button type="submit">
|
||||
|
@ -26,7 +26,7 @@ First written: 2023/1/26
|
||||
{% block content %}
|
||||
|
||||
<div class="mb-2 accounting-toolbar">
|
||||
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.base-account.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||
<button type="submit">
|
||||
|
@ -32,7 +32,7 @@ First written: 2023/2/6
|
||||
{{ A_("New") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting.currency.list") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||
<button type="submit">
|
||||
|
@ -19,7 +19,7 @@ description-editor-modal.html: The modal of the description editor
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/28
|
||||
#}
|
||||
<form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" name="accounting-dummy-form" data-debit-credit="{{ description_editor.debit_credit }}">
|
||||
<form id="accounting-description-editor-{{ description_editor.debit_credit }}" class="accounting-description-editor" data-debit-credit="{{ description_editor.debit_credit }}">
|
||||
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
@ -19,7 +19,7 @@ journal-entry-line-item-editor-modal: The modal of the journal entry line item e
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/2/25
|
||||
#}
|
||||
<form id="accounting-line-item-editor" name="accounting-dummy-form">
|
||||
<form id="accounting-line-item-editor">
|
||||
<div id="accounting-line-item-editor-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-line-item-editor-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
@ -19,7 +19,7 @@ recurring-item-editor-modal.html: The modal of the recurring item editor
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/3/22
|
||||
#}
|
||||
<form id="accounting-recurring-item-editor-{{ expense_income }}" name="accounting-dummy-form">
|
||||
<form id="accounting-recurring-item-editor-{{ expense_income }}">
|
||||
<div id="accounting-recurring-item-editor-{{ expense_income }}-modal" class="modal fade" tabindex="-1" aria-labelledby="accounting-recurring-item-editor-{{ expense_income }}-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
@ -19,7 +19,7 @@ search-modal.html: The search modal
|
||||
Author: imacat@mail.imacat.idv.tw (imacat)
|
||||
First written: 2023/3/8
|
||||
#}
|
||||
<form action="{{ url_for("accounting-report.search") }}" name="accounting-search-form" method="get" role="search" aria-labelledby="accounting-search-modal-label">
|
||||
<form action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-search-modal-label">
|
||||
<div class="modal fade" id="accounting-search-modal" tabindex="-1" aria-labelledby="accounting-search-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
@ -118,7 +118,7 @@ First written: 2023/3/8
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if use_search %}
|
||||
<form class="btn btn-primary d-flex input-group" name="accounting-search-form" action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||
<form class="btn btn-primary d-flex input-group" action="{{ url_for("accounting-report.search") }}" method="get" role="search" aria-labelledby="accounting-toolbar-search-label">
|
||||
<input id="accounting-toolbar-search" class="form-control form-control-sm" type="search" name="q" value="{{ request.args.q }}" placeholder=" " required="required">
|
||||
<label id="accounting-toolbar-search-label" for="accounting-toolbar-search" class="input-group-text">
|
||||
<button type="submit">
|
||||
|
@ -14,7 +14,8 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""The utilities to cast values into desired types, to avoid IDE warnings.
|
||||
"""The utility to cast a SQLAlchemy column into the column type, to avoid
|
||||
warnings from the IDE.
|
||||
|
||||
This module should not import any other module from the application.
|
||||
|
||||
|
@ -41,8 +41,11 @@ def inherit_next(uri: str) -> str:
|
||||
:param uri: The URI.
|
||||
:return: The URI with the current next URI added at the query argument.
|
||||
"""
|
||||
next_uri: str | None = __get_next_uri()
|
||||
return uri if next_uri is None else __set_next(uri, next_uri)
|
||||
next_uri: str | None = request.form.get("next") \
|
||||
if request.method == "POST" else request.args.get("next")
|
||||
if next_uri is None:
|
||||
return uri
|
||||
return __set_next(uri, next_uri)
|
||||
|
||||
|
||||
def or_next(uri: str) -> str:
|
||||
@ -51,22 +54,9 @@ def or_next(uri: str) -> str:
|
||||
:param uri: The URI.
|
||||
:return: The next URI or the supplied URI.
|
||||
"""
|
||||
next_uri: str | None = __get_next_uri()
|
||||
return uri if next_uri is None else next_uri
|
||||
|
||||
|
||||
def __get_next_uri() -> str | None:
|
||||
"""Returns the valid next URI.
|
||||
|
||||
:return: The valid next URI.
|
||||
"""
|
||||
next_uri: str | None = request.form.get("next") \
|
||||
if request.method == "POST" else request.args.get("next")
|
||||
if next_uri is None or not next_uri.startswith("/"):
|
||||
return None
|
||||
if len(next_uri) > 512:
|
||||
return next_uri[:512]
|
||||
return next_uri
|
||||
return uri if next_uri is None else next_uri
|
||||
|
||||
|
||||
def __set_next(uri: str, next_uri: str) -> str:
|
||||
|
@ -52,8 +52,6 @@ def create_app(is_testing: bool = False) -> Flask:
|
||||
db_uri: str = "sqlite:///" if is_testing else "sqlite:///local.sqlite"
|
||||
app.config.from_mapping({
|
||||
"SECRET_KEY": os.environ.get("SECRET_KEY", token_urlsafe(32)),
|
||||
"SESSION_COOKIE_SAMESITE": "Lax",
|
||||
"SESSION_COOKIE_SECURE": True,
|
||||
"SQLALCHEMY_DATABASE_URI": db_uri,
|
||||
"BABEL_DEFAULT_LOCALE": "en",
|
||||
"ALL_LINGUAS": "zh_Hant|正體中文,en|English,zh_Hans|简体中文",
|
||||
|
@ -19,12 +19,10 @@
|
||||
"""
|
||||
from babel import Locale
|
||||
from flask import request, session, current_app, Blueprint, Response, \
|
||||
redirect, Flask
|
||||
redirect, url_for, Flask
|
||||
from flask_babel import Babel
|
||||
from werkzeug.datastructures import LanguageAccept
|
||||
|
||||
from accounting.utils.next_uri import or_next
|
||||
|
||||
bp: Blueprint = Blueprint("locale", __name__, url_prefix="/")
|
||||
|
||||
|
||||
@ -70,7 +68,9 @@ def set_locale() -> Response:
|
||||
all_linguas: dict[str, str] = get_all_linguas()
|
||||
if "locale" in request.form and request.form["locale"] in all_linguas:
|
||||
session["locale"] = request.form["locale"]
|
||||
return redirect(or_next("/"))
|
||||
if "next" in request.form:
|
||||
return redirect(request.form["next"])
|
||||
return redirect(url_for("home.home"))
|
||||
|
||||
|
||||
def get_all_linguas() -> dict[str, str]:
|
||||
|
@ -101,60 +101,6 @@ class NextUriTestCase(unittest.TestCase):
|
||||
"name": "viewer"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_invalid(self) -> None:
|
||||
"""Tests the next URI utilities without an invalid next URI.
|
||||
|
||||
:return: None.
|
||||
"""
|
||||
def test_invalid_next_uri_view() -> str:
|
||||
"""The test view without the next URI."""
|
||||
self.assertEqual(inherit_next(self.TARGET),
|
||||
request.args.get("inherit-expected"))
|
||||
self.assertEqual(or_next(self.TARGET),
|
||||
request.args.get("or-expected"))
|
||||
return ""
|
||||
|
||||
self.app.add_url_rule("/test-invalid-next",
|
||||
view_func=test_invalid_next_uri_view,
|
||||
methods=["GET", "POST"])
|
||||
client: httpx.Client = httpx.Client(app=self.app, base_url=TEST_SERVER)
|
||||
client.headers["Referer"] = TEST_SERVER
|
||||
csrf_token: str = get_csrf_token(client)
|
||||
next_uri: str
|
||||
expected1: str
|
||||
expected2: str
|
||||
response: httpx.Response
|
||||
|
||||
# A foreign URI
|
||||
next_uri = "https://example.com"
|
||||
expected1 = self.TARGET
|
||||
expected2 = self.TARGET
|
||||
response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}"
|
||||
f"&inherit-expected={quote_plus(expected1)}"
|
||||
f"&or-expected={quote_plus(expected2)}")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = client.post("/test-invalid-next"
|
||||
f"?inherit-expected={quote_plus(expected1)}"
|
||||
f"&or-expected={quote_plus(expected2)}",
|
||||
data={"csrf_token": csrf_token,
|
||||
"next": next_uri})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# An extremely-long URI to trigger the error
|
||||
next_uri = "/" + "x" * 1024
|
||||
expected2 = next_uri[:512]
|
||||
expected1 = f"{self.TARGET}?next={quote_plus(expected2)}"
|
||||
response = client.get(f"/test-invalid-next?next={quote_plus(next_uri)}"
|
||||
f"&inherit-expected={quote_plus(expected1)}"
|
||||
f"&or-expected={quote_plus(expected2)}")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = client.post("/test-invalid-next"
|
||||
f"?inherit-expected={quote_plus(expected1)}"
|
||||
f"&or-expected={quote_plus(expected2)}",
|
||||
data={"csrf_token": csrf_token,
|
||||
"next": next_uri})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class QueryKeywordParserTestCase(unittest.TestCase):
|
||||
"""The test case for the query keyword parser."""
|
||||
|
Loading…
Reference in New Issue
Block a user