表单提交交给谁?收邮箱那张表一半没写去向

表单提交交给谁?收邮箱那张表一半没写去向
张文保 22 分钟阅读 2,417 阅读
本文目录
  1. 按下提交那一刻,浏览器照着谁的话做?
  2. 这一屋子表单里,有几张是给人填的?
  3. 收邮箱那张表,action写的是什么?
  4. 跨域的那12个,去了哪几个域名?
  5. 隐藏域里装的到底是什么?
  6. 名单标签:你被分进哪一档,写在框边上
  7. 一个页面把访客的公网IP写进了隐藏域
  8. 最长的那个值有2589个字符
  9. 没写method的那些,如果那段脚本没接住会怎样?
  10. 原始HTML里没有、渲染之后才冒出来的表单
  11. 表单里那个叫id的字段,把表单自己的名字顶掉了
  12. 读数取决于你等多久、点没点
  13. 照着改,先动哪几处?
  14. 常见问题解答
  15. 表单本来就是JavaScript提交的,action写不写真的有区别吗?
  16. 订阅表单直接提交到邮件服务商的域名,有没有问题?
  17. 隐藏域里放商品ID、来源标签这些,算不算多余的字节?
  18. 怎么快速查一遍自己站上的表单去向?
  19. 为什么页面上form元素那么多,能填的却那么少?
  20. 用curl抓HTML来做表单审计够不够?
  21. 权威参考资料

摘要:接着上一篇的样本往下拆,这次数的是表单本身。55个站的191个页面上共有985张form元素,站内按签名去重后362张,其中当场有可见可填项的只有120张,33.1%。收邮箱那一格去重后182个,源码里写明了提交地址的85个——46.7%。剩下的一半,去向要么写在JavaScript里,要么根本不在这一页上。另有12个邮箱框直接把地址写成了别人的域名,767个隐藏域里躺着访客的公网IP、浏览器名字和名单标签。

上一篇数的是输入框自己说了什么:这一格要邮箱还是要电话,允不允许浏览器代填,空着能不能提交。表单字段那四件事写不写,取决于这个框是谁加上去的。

这一篇往后挪一格:用户把字打完,按下那个按钮,东西去哪儿。

这件事页面上永远不会写。没有哪个站会在订阅框底下加一行小字说“你的邮箱将被提交到manage.kmail-lists.com”。但在源码里它是有位置的——form元素上的action属性,就是干这个的。所以这一轮的问题很单纯:那个位置上,到底写了什么。

按下提交那一刻,浏览器照着谁的话做?

先把这条路径摊开。一个规规矩矩的表单提交,浏览器读三样东西:action决定往哪个地址发,method决定用GET还是POST,enctype决定怎么打包。三样都不写也能跑——按MDN对form元素的说明,action缺省是当前页地址,method缺省是GET。

但今天大部分站不走这条路。前端脚本会拦下提交事件,自己拿fetch把数据送出去。这时候action写什么都不影响真实去向,它变成一个纯粹的声明。

声明没用吗?有三个场合它照样管事:

  • 脚本还没加载完、加载失败、被拦截器挡掉的时候,浏览器会退回去照HTML做;
  • 读页面的程序——搜索引擎、比价插件、AI代理——只看得见HTML,看不见那段脚本的意图;
  • 你自己排查问题的时候,它是唯一一个不用打开调试器就能看到的线索。

所以这一轮量的是声明,不是流量。这里得先说清楚边界:本文没有真的提交过任何一张表单,所有结论都是“如果按HTML写的去做会怎样”。这一点跟代理能不能在你站上做事那一轮的口径一致——那次判的是机器能不能构造出请求,这次问的是这张表交给谁。

这一屋子表单里,有几张是给人填的?

55个站、191个页面,取到985张form元素。同一个站里action、method、控件数、隐藏域名字、按钮文案全都一样的算同一张,去重后362张,每站中位6张。

然后按“有没有一格是给人填的”分开数:

这张表单长什么样张数占比
当场有可见可填项(真表单)12033.1%
有可填项,但当场一个都看不见12835.4%
只有隐藏域,没有一格给人填8623.8%
完全空的,一个控件都没有287.7%

三分之一是给人填的,剩下三分之二各有各的用途。第三档那86张最典型:action写着/cart/add的32张、/localization的17张、/cart的13张、/api/cart的7张。加购按钮是一张表单,切换国家和币种是一张表单,购物车更新是一张表单。那批/localization表单背后是地区与币种的切换,发货范围与退货范围对不上那一轮量的就是这条线的另一头。它们借用form这个壳,是因为它天生就能带一堆参数走。

这个用法本身没毛病,反而是规规矩矩的老派写法。有意思的是比例:页面上的form元素,多数不是用来收用户输入的,是站点自己跟后端说话的通道。所以“这一页有几张表单”这个数,对判断用户体验几乎没有参考价值。

再看真表单那120张有多小:可见可填项只有1个的69张,2个的32张。八成的真表单是一两个格子的小东西——页脚订阅框、搜索框、登录框。那种十几个字段的长表单在这批站上是稀有物。搜索框那一格尤其典型,它是站上被用得最多的输入框,实现上却最单薄——站内搜索页那一轮实测把它下游那一页的问题也数过一遍。

顺带记一笔:整整191个页面里,一个form元素都没有的只有7个,占3.7%;其中dreametech的四个页面全都没有。这跟首页正文空壳那一轮的结论对得上,能读到的东西越少的站,越是什么标记都不写。

收邮箱那张表,action写的是什么?

选邮箱这一格的理由跟上一篇一样:它是这批站上唯一一件人人都在做的事,51个站都有,横着比才公平。去重后182个邮箱框,先看它们挂在谁名下。

167个在某张form里,占91.8%;15个不在任何form里,占8.2%,出自10个站——aloyoga、cluse、everlane、gymshark、jackery、liquiddeath、mackweldon、misen、reebok、taylorstitch。这15个框,去向连个位置都没有。

剩下167个里,action那一栏是这么分布的:

action写了什么个数占在form里的比例
压根没写(提交回当前页)8249.1%
写了本站地址7343.7%
写了别人的域名127.2%

把不在form里的那15个也算进来,结论是:182个收邮箱的框里,源码里能看出去向的85个,46.7%。另外那一半,你在HTML里翻不到任何线索——得打开调试器,或者干脆去读那段打包过的脚本。

写了本站地址的那73个也值得看一眼。前几名是/account/login(18个)、/account/recover(15个)、/contact#contact_form(7个)。这些全是电商平台默认主题里的原件,跟上一篇那条规律接得严丝合缝:写得全的那些框,几乎都是平台替你写的

跨域的那12个,去了哪几个域名?

12个不多,但它们是这一轮最干净的样本——因为它们把去向写在了明处。

  • manage.kmail-lists.com:8个;
  • a.klaviyo.com:2个;
  • app.powerfulform.com:2个。

前两个是同一家邮件营销服务的两个入口,第三个是表单托管工具。涉及10个站:beistravel、fahertybrand、glossier、liquiddeath、magicspoon、marinelayer、monos、outdoorvoices、rothys、taylorstitch。

用户在这些站的页脚填一个邮箱按下订阅,浏览器发出的那个请求,收件人不是这个品牌的服务器。这件事从合规角度讲得通——服务商是数据处理方,隐私政策里通常也提到了;从用户视角讲,页面上一个字都没有。顺着这条线还有更早的一层:USENIX那份Leaky Forms研究实测过十万量级的站点,发现相当一部分站在用户按下提交之前,邮箱就已经被页面上的第三方脚本取走了。action写给谁,只是这条链路上最后一环。

更值得注意的是这12个框在上一篇里的表现:第三方托管的那一档,名字写得最漂亮(94.6%有正经名字),autocomplete几乎为零(8.1%)。营销工具在乎表单看起来专业,不在乎浏览器能不能替用户少打二十个字符。第三方脚本两天半就自己变一批这件事在这里也一样成立:这12张表单的写法什么时候变、变成什么样,不由站点定。

顺带说一句,跨域这件事在同一批页面上还有个更极端的形态。parachutehome的商品页里藏着一张action指向https://www.facebook.com/tr/的表单,整张display:none,里面83到160个type=text的输入框,名字是id、ev、dl、ts、ud[external_id]这种——那是跟踪像素把自己的请求参数做成了输入框。它不收用户输入,它只是借form这个壳装数据。这一张在上一篇里差点把统计口径带沟里去,最后被单独剔了出来。

隐藏域里装的到底是什么?

362张表单一共767个隐藏域,95.2%带着值,值长度中位数只有7个字符。按名字归类之后是这样:

装的是什么个数典型名字
表单自述(我是哪种表单、用什么方法)402form_type、utf8、_method、type
商品与购物车93product-id、id、quantity、selling_plan
页面上下文55return_to、checkout_url、page[href]
地区与语言47country_code、locale_code、options[prefix]
来路追踪27login_with_shop[analytics_trace_id]、$source
名单与同意19contact[tags]、$consent、Interest
用户信息11customer[email]、customer[id]、attributes[...]
令牌与校验1g-recaptcha-response

名单标签:你被分进哪一档,写在框边上

contact[tags]这个字段出现11次,值很有意思:allbirds、awaytravel、drinkolipop、baseus写的是newsletter,casper写的是footer,url-homepage——它连你是从页脚还是从首页订阅的都记下来了;aloyoga写的是cc:SG,rc:SG,国家和地区各一份。taylorstitch那张表里还有个Interest字段,值是Men,另有一个$consent写着web。marinelayer的订阅表单里躺着一个Sign Up Source,值是Web Footer Sign-up

这些都不是错。给订阅来源打标签是邮件营销的基本功。值得留意的只是一点:用户看到的是一个只要邮箱的框,实际提交出去的是邮箱加上一小串关于他的判断。这跟浏览器本地存储里那些服务器删不掉的键是同一类东西:数据的边界比界面显示的宽。

一个页面把访客的公网IP写进了隐藏域

flyingtiger的购物车表单里有一组attributes[...]attributes[customer-account]值是logged outattributes[browser-name]值是Google Chromeattributes[public-ip-address]值是一个完整的公网IP地址。

这是把访客的环境信息预先写进了页面HTML,等表单提交时一并带走。技术上完全能理解——多半是给风控或者物流分区用的。但它意味着两件事:这个IP出现在了页面源码里,任何拿得到这一页的人都能读;而这一页如果被缓存住,缓存里也会留着一个具体访客的IP。HTML注释里那些构建日期和供应商名单是同一类泄漏,只是这一次泄的是用户自己的东西。

最长的那个值有2589个字符

italic的商品页上有个叫cartFormInput的隐藏域,值是一大段JSON,最长的一份2589个字符,里面装着整个购物车的商品清单。同一个站的另一个隐藏域叫analytics,值也是JSON,开头是{"products":[{"id":"gi...。liquiddeath的_keyLabel是582个字符。

这些字节每次渲染都要发一遍,而且是不可缓存的那一部分——首页字节里58%缓存寿命为零那一轮量过这笔账,隐藏域里的JSON正是其中一种。

没写method的那些,如果那段脚本没接住会怎样?

167个在form里的邮箱框,method这一栏:POST的101个,明确写GET的9个,一个字没写的57个。没写就是GET,这是标准定的缺省值。

加起来66个邮箱框,按HTML写的做的话会走GET。GET与POST的差别就在这一步:GET提交意味着表单数据被拼进地址栏的查询串——邮箱会出现在URL里,出现在浏览器历史里,出现在服务器访问日志里,还会跟着Referer头传给这一页上的第三方资源。

现实中这几乎不会发生,因为那段脚本会拦下提交。但“几乎”这个词值多少钱,取决于你的脚本有多可靠:CSP白名单漂移会把脚本拦下来SRI哈希对不上脚本会一行都不执行,广告拦截器和企业代理也会。这些场景下,浏览器会老老实实照HTML里写的那句话去做。

所以这一栏该怎么填,其实是句大白话:你希望脚本挂掉的时候发生什么,就在这两个属性里写什么。写POST加一个真实能收数据的地址,最坏情况是页面跳转一次但数据收到了;什么都不写,最坏情况是把用户的邮箱糊在地址栏上。

原始HTML里没有、渲染之后才冒出来的表单

每个页面在读完渲染后的DOM之后,又原样取了一次它的HTML源码,两边对照。190个页面里:

  • form数量一致的71个,37.4%;
  • 渲染后变多的81个,42.6%;
  • 渲染后反而变少的38个,20.0%;
  • 原始HTML里一张form都没有、渲染之后才有的27个。

input这一层差距更大:数量一致的只有19个页面,渲染后变多的139个。

变多好理解——弹层、抽屉、评价组件都是脚本插进来的。变少那20%才需要解释一下:脚本会把服务端渲染的那一份替换掉,重建时合并了几张表,或者干脆把某些不用的删了。只看HTML看不到那8个第三方域名说的是同一件事的另一面:你以为在读页面,其实读的只是页面的初始状态。

这对做审计的人有个直接后果:拿curl抓一份HTML去数表单,会漏掉四成多的页面上的东西;但只读渲染后的DOM,又会看不见那20%被替换掉的原件。两边都得看。

表单里那个叫id的字段,把表单自己的名字顶掉了

这一轮的脚本第一版在44个页面上直接崩了,报错是“这个值上没有replace方法”。查下去发现的东西,比那个报错本身有意思得多。

HTML标准的表单控件章节给form元素开了个后门:表单里那些控件的name和id,会被挂成表单对象自己的属性,而且优先级高过表单原本的属性。商品页上的加购表单通常有个<input name="id">装着变体ID,于是读form.id读回来的不是字符串,是那个输入框元素本身。

全样本里275张表单实例中招,出自33个站。被顶掉的属性绝大多数是id(274次),还有1次是submit——那张表单里有个name叫submit的控件,把表单的提交方法顶掉了,脚本要是调用form.submit()就会当场报错。

这个坑对写审计脚本的人是个直接教训:读表单属性一律用getAttribute,别用点号。对做站的人则是另一条:给控件起名字的时候,避开id、name、action、method、submit这几个词。字段名的写法能引出多少麻烦,这算是最物理的一种。

读数取决于你等多久、点没点

上面所有百分比,量的都是同一个状态:页面载入后等9秒左右、不做任何操作。这个状态到底有多不稳,另挑了29个首页做对照,同一个地址读三次——等5秒、再等20秒、然后替用户点一下页面上的订阅或搜索入口。

对照读数变了的页面
只多等20秒,form数变了17.2%
只多等20秒,可填控件数变了20.7%
点一下之后,form数变了26.1%
点一下之后,可填控件数变了56.5%
点了完全没变化39.1%

总量上看更直白:这29个首页上的可见可填控件,等5秒时37个,等25秒时42个,点一下之后70个。接近一半的输入框,是在有人动手之后才存在的。

几个具体的:gymshark首页原本一个form都没有,点开那个写着Email Sign Up的按钮之后冒出1张表单7个控件;jackery点完订阅按钮,form从2张变成5张;aloyoga点开账号入口,控件从2个变成6个;brooklinen什么都不点,光是多等20秒,form就从1张变成3张。

反过来,39.1%的页面点了也没有任何变化——那些框本来就在DOM里,点击只是掀开了一层样式。这跟隐藏内容那份裁决表讲的是同一件事:藏起来有很多种藏法,机器算不算数要看藏法。

照着改,先动哪几处?

四条,按性价比排。

第一条,给每个收邮箱、收询盘的表单补上一个真实可用的action和method。不是为了让浏览器去提交,是为了脚本挂掉那一刻页面还有个说法。做法很简单:后端提供一个能收表单编码数据的端点,写成method="post" action="/xxx",前端脚本照旧拦截。这一步只花几分钟,收益是把一半查不到去向的表单变成查得到。

第二条,把那些不在任何form里的输入框放回form里。182个邮箱框里有15个是孤儿,10个站中招。放回去顺手就能解决一串问题:回车键能提交了、密码管理器认得出来了、代理程序能看出这几个格子是一组了。

第三条,翻一遍自己站上的隐藏域,看看有没有本来不该出现在HTML里的东西。判据是问三个问题:这个值是不是关于某一个具体访客的?它会不会被缓存住?拿到这一页的人读到它要不要紧?公网IP、账号状态、完整的购物车JSON,都得按这三条过一遍。合规架构那篇讲的同意与数据边界,落到实现层就是这一堆<input type="hidden">

第四条,做审计时两边都读。只读HTML漏掉四成多,只读渲染后的DOM看不见被替换掉的两成。保哥自己那套体检脚本现在是固定跑两遍,两份对不上的页面单独列出来人工看,成本不高,能挡掉大部分误判。

最后说一句选型上的判断。form这个元素在很多前端框架里已经被当成可有可无的壳,数据用状态管住,提交用fetch发出去,看起来更干净。但这一轮的数据摆在这儿:那个壳是页面上唯一一处能写下“这些格子是一组、它们要去哪儿”的地方。省掉它,省的是十几个字符,丢的是这一页对外唯一的一份说明书。换个平台只是换一批老代码那篇讲过前端习惯的惯性有多大,这一条同样是惯性的产物。代理时代那笔账已经算过一次,这里只是再补一个更基础的注脚。

常见问题解答

表单本来就是JavaScript提交的,action写不写真的有区别吗?

有。三种场合它会真的生效:脚本没加载完就有人按了回车、脚本被CSP或者拦截器挡掉、用户的网络中途断了又恢复。另外读页面的程序只看HTML,你写了它就知道这张表往哪儿提交,不写它只能猜。成本是十几个字符,不值得省。

订阅表单直接提交到邮件服务商的域名,有没有问题?

技术上没问题,合规上要看你有没有在隐私政策里把这家服务商列为数据处理方。真正要注意的是另一件事:这类表单的HTML由服务商生成,它的字段声明写得好不好、什么时候变,都不由你定。本轮那12个跨域邮箱框的autocomplete覆盖率只有8.1%,就是这么来的。

隐藏域里放商品ID、来源标签这些,算不算多余的字节?

算,但通常值。要警惕的是两类:一是关于具体访客的值,比如IP、账号状态、完整购物车JSON,它们会跟着页面一起被缓存;二是几千字符的大块JSON,那部分字节每次渲染都要重发。判据是问一句:这个值能不能在提交那一刻由脚本现算出来。

怎么快速查一遍自己站上的表单去向?

控制台里跑一句[...document.querySelectorAll('form')].map(f=>[f.getAttribute('action'),f.getAttribute('method'),f.elements.length])就能看个大概。注意一定要用getAttribute,别写f.action——表单里只要有个name叫action的控件,点号读回来的就是那个控件而不是地址。

为什么页面上form元素那么多,能填的却那么少?

因为form这个元素在电商模板里被大量用作参数容器:加购、切换国家币种、更新购物车,全都是一张表单。本轮362张去重后的表单里,只有隐藏域的86张,完全空的28张。所以“这一页有几张表单”对判断用户体验没什么参考价值,得按有没有可见可填项再分一次。

用curl抓HTML来做表单审计够不够?

不够,但也不能不抓。本轮190个页面里,渲染后form数量变多的占42.6%,只抓HTML会漏掉这一批;而渲染后变少的占20.0%,只读DOM又会看不见原件。两份都取,对不上的单独看,这是目前最省事的做法。

权威参考资料

分享到
标签
版权声明

本文标题:《表单提交交给谁?收邮箱那张表一半没写去向》

本文链接:https://zhangwenbao.com/form-submission-destination-audit.html

版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交