-
-
Notifications
You must be signed in to change notification settings - Fork 694
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
Writable canned queries with magic parameters fail if POST body is empty #967
Comments
This is so weird. In the test I wrote for this the following passed:
But without the
Here's the test I wrote: def test_magic_parameters_json_body(magic_parameters_client):
magic_parameters_client.ds._metadata["databases"]["data"]["queries"]["runme_post"][
"sql"
] = "insert into logs (line) values (:_header_host)"
response = magic_parameters_client.post("/data/runme_post?_json=1", {}, csrftoken_from=True)
assert response.status == 200
assert response.json["ok"], response.json
post_actual = magic_parameters_client.get(
"/data/logs.json?_sort_desc=rowid&_shape=array"
).json[0]["line"] |
So the mystery here is why does omitting |
Relevant code: datasette/datasette/views/database.py Lines 222 to 236 in 853c5fc
This issue may not be about |
Is the bug here that magic parameters are incompatible with CSRF-exempt requests (e.g. request with no cookies)? |
Hunch: I think the |
New (failing) test: @pytest.mark.parametrize("use_csrf", [True, False])
@pytest.mark.parametrize("return_json", [True, False])
def test_magic_parameters_csrf_json(magic_parameters_client, use_csrf, return_json):
magic_parameters_client.ds._metadata["databases"]["data"]["queries"]["runme_post"][
"sql"
] = "insert into logs (line) values (:_header_host)"
qs = ""
if return_json:
qs = "?_json=1"
response = magic_parameters_client.post(
"/data/runme_post{}".format(qs),
{},
csrftoken_from=use_csrf or None,
allow_redirects=False,
)
if return_json:
assert response.status == 200
assert response.json["ok"], response.json
else:
assert response.status == 302
messages = magic_parameters_client.ds.unsign(
response.cookies["ds_messages"], "messages"
)
assert [["Query executed, 1 row affected", 1]] == messages
post_actual = magic_parameters_client.get(
"/data/logs.json?_sort_desc=rowid&_shape=array"
).json[0]["line"]
assert post_actual == "localhost" It passes twice, fails twice - failures are for the ones where |
While I'm running the above test, in the rounds that work the In the rounds that fails it returns So it looks like the |
Yes! The tests all pass if I update the test function to do this: response = magic_parameters_client.post(
"/data/runme_post{}".format(qs),
{"ignore_me": "1"},
csrftoken_from=use_csrf or None,
allow_redirects=False,
) So the bug only occurs if the POST body is completely empty. |
So the problem actually occurs when the Relevant code: datasette/datasette/views/database.py Lines 228 to 236 in 853c5fc
And: datasette/datasette/views/database.py Lines 364 to 383 in 853c5fc
I'm passing a special magic parameters dictionary for the Python I tracked down the relevant C code: Py_BEGIN_ALLOW_THREADS
num_params_needed = sqlite3_bind_parameter_count(self->st);
Py_END_ALLOW_THREADS
if (PyTuple_CheckExact(parameters) || PyList_CheckExact(parameters) || (!PyDict_Check(parameters) && PySequence_Check(parameters))) {
/* parameters passed as sequence */
if (PyTuple_CheckExact(parameters)) {
num_params = PyTuple_GET_SIZE(parameters);
} else if (PyList_CheckExact(parameters)) {
num_params = PyList_GET_SIZE(parameters);
} else {
num_params = PySequence_Size(parameters);
}
if (num_params != num_params_needed) {
PyErr_Format(pysqlite_ProgrammingError,
"Incorrect number of bindings supplied. The current "
"statement uses %d, and there are %zd supplied.",
num_params_needed, num_params);
return;
} It looks to me like this should fail if the number of keys known to be in the dictionary differs from the number of named parameters in the query. But if those numbers fail to match it still works as far as I can tell - it's only dictionary length of 0 that is causing the problems. |
I wish I could call https://www.sqlite.org/c3ref/bind_parameter_count.html and https://www.sqlite.org/c3ref/bind_parameter_name.html from Python. Might be possible to do that using param_count = lib.sqlite3_bind_parameter_count(self.statement)
for idx in range(1, param_count + 1):
param_name = lib.sqlite3_bind_parameter_name(self.statement,
idx) |
I think the easiest fix is for me to ensure that calls to |
When I try to use the new
?_json=1
feature from #880 with magic parameters from #842 I get this error:The text was updated successfully, but these errors were encountered: