The module is actually using the following code, to add the time restriction form field.
$form['honeypot_time'] = [
'#type' => 'hidden',
'#title' => t('Timestamp'),
'#default_value' => honeypot_get_signed_timestamp(time()),
'#element_validate' => ['_honeypot_time_restriction_validate'],
'#cache' => [
'max-age' => 0,
],
];
Since the value is passed to the browser, and it could be changed, the module needs to join the timestamp with its HMAC value, to be sure it is not changed. There is no need to use the HMAC, to be sure the value is not changed: It would be enough to use value as type of the form element. In this way, the value would not be passed to the browser, and its value could not be changed from the user who submits the form.
$form['honeypot_time'] = [
'#type' => 'value',
'#value' => time(),
'#cache' => [
'max-age' => 0,
],
];
If the form field is created as hidden to verify the user changes its value, and reject the form submission in that case, the module could be use two form fields: The module would check the hidden field is not changed, and then use the value field to see if the time restriction has been respected.
Comment | File | Size | Author |
---|---|---|---|
#25 | use_type_value-2672826-25.patch | 3.81 KB | geerlingguy |
| |||
#22 | use_type_value-2672826-22.patch | 3.84 KB | mr.baileys |
| |||
#9 | use_type_value-2672826-9-D8-revert.patch | 5.78 KB | mr.baileys |
| |||
#9 | use_type_value-2672826-9-D8-test-only.patch | 1.43 KB | mr.baileys |
#5 | use_type_value-2672826-5.patch | 4.97 KB | mr.baileys |
|
Comments
Comment #2
apadernoAs side note, since both the timestamp and its HMAC are visible from the browser, could not a user get the value from a form, and use it for other forms he submits? A bot could do that, I guess.
Comment #3
apadernoComment #4
mr.baileysThis makes a lot of sense, and simplifies the module's logic without trade-offs with regards to security.
Patch attached with switches to #type => 'value', rips out the timestamp signing logic and removes time limit from validation (since the value is no longer posted back, it cannot be tampered with and no validation should be necessary).
Tests will fail (2 fails) since testing is currently broken (#2673060: Tests are currently failing.)
Comment #5
mr.baileysForgot the patch.
Comment #6
geerlingguy CreditAttribution: geerlingguy as a volunteer commentedWorks well for me. Will merge in a minute.
Comment #8
geerlingguy CreditAttribution: geerlingguy as a volunteer commentedThis makes the code a bit simpler (always a nice thing), and I'm not quite sure why I didn't go this route originally when building the module... backport to D7 shouldn't be too difficult; much of the structure is the same. I'll get to it if I get a chance, otherwise, more internet points to @mr.baileys if you want to do it :)
Comment #9
mr.baileysI'm not 100% sure yet, but we might have to revert this change.
While porting this to D7, I noticed that I removed the #default_value from the timestamp element completely instead of leaving time(). This caused the time protection to always be triggered:
$honeypot_time was always '', since I removed the #default_value.
This also means that there is no test coverage for a "not-too-fast-form-submission-when-time-limit-enabled"-scenario, and I set about writing that test.
I'm unable to get the test to pass with #type => 'value' though, and it seems there are subtle differences between 'value' and 'hidden': 'Hidden' allows you to set a #default_value, which serves as initial value for the field. 'Value' only has '#value', not '#default_value', and that value seems to be reset/refreshed each time the form is loaded. This means that even though we set the timestamp for honeypot_time on initial form retrieval, the value is updated just prior to validation, and each submission is considered too fast.
Attached is a patch containing the amended test that *should* pass (but is expected to fail against HEAD at the moment), as well as a combined revert+test patch.
Comment #12
mr.baileysHuh. Version was changed to D8, but seems the testbot still tried to apply the patch against D7.
Comment #13
mr.baileysSwitching to bug since we've introduced a bug here, switching to major since this breaks time limit protected forms.
Comment #14
apadernoNotice that #element_validate cannot be used for a value element; the only attribute it accepts is #value.
Comment #15
geerlingguy CreditAttribution: geerlingguy as a volunteer commentedK... We can revert to stop the bleeding, then after that; is there any way we could still use value successfully? I do like the aspect of the value never hitting the end user in any way.
Comment #16
mr.baileys@kiamlaluno: interesting, the validation handler is actually invoked though.
@geerlinguy: I spent some time troubleshooting this today, but was unable to have the value *not* refresh prior to validation. I'll take a closer look later tonight to see if I can come up with a viable solution...
Comment #17
apadernoThe code currently in the development snapshot is the following.
The form element is wrong, since it doesn't have any #value. The correct one is the following one.
You cannot add an element validation handler, so a form validation handler should be added. I am not sure #cache is accepted from that kind of form element.
Comment #18
mr.baileysThe dev-snapshot is indeed incorrect, but I've been running some tests locally with the correct definitions, and am still running into test failures. This seems related, although old so I don't know if it is still releveant: [#169020]
Comment #19
geerlingguy CreditAttribution: geerlingguy as a volunteer commentedRelevant snippet from that forum topic:
Will need to look into Form API to confirm, but that sounds potentially true of the existing system... I've never used it for a value that is dynamic.
Comment #21
geerlingguy CreditAttribution: geerlingguy as a volunteer commentedMerged the revert patch, so now back to needs work/feature request... I'm not sure if this will be possible if the FAPI is doing what that forum topic suggests.
Comment #22
mr.baileysI've picked this up again at the Drupal Dev Days.
From a Form API perspective, I don't think what we are trying to do (saving state for the form on the initial GET request) is possible. Drupal Core actively avoids persisting form and form_state for a GET request, regardless of whether you are logged in or not. This seems by design, see for example #2502785: Remove support for $form_state->setCached() for GET requests. This also causes the code that assigns the timestamp to run again, defeating it's purpose.
However, we can still avoid the process of signing/tamper-proofing the timestamp in the form by storing the timestamp server-side ourselves. Patch attached generates a unique id per form (we cannot use form_build_id for this, since it is not yet available when initially rendering the form), and stores the timestamp in
\Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface
. When submitting, the timestamp is retrieved from that store to check whether enough time has elapsed for the form submission to pass the Honeypot time restriction. If no timestamp can be found, the submission is rejected.Comment #23
geerlingguy CreditAttribution: geerlingguy as a volunteer commentedI'm going to try to do some manual testing just to make sure I'm not missing something here, but I like the approach.
Comment #24
geerlingguy CreditAttribution: geerlingguy as a volunteer commentedNew patch attached (needed reroll after merging #2853205: Field is not hidden if big pipe is used). There are a couple bits of code I want to also touch up.
Comment #25
geerlingguy CreditAttribution: geerlingguy as a volunteer commentedPatch attached this time.
Comment #27
geerlingguy CreditAttribution: geerlingguy as a volunteer commented