本文へスキップstate=CHASE vy+=G*dt shake(0.3) perlin(u,v) fadeOut(1.0) RAYCAST LIGHTMAP MINIBOSS cross(a,b) sign(vx) FRAME if(combo>best) WEBGL moveTo(next) ease=t*t SKELETON vy=min(vy,MAX) ox=cx+cos(a)*d if(see(player)) perlin(u,v) orbit(t) shuffle(deck) FADE oy=cy+sin(a)*d present() dot(n,l) HITBOX MESH vx*=0.9 UPGRADE hitstop(0.05) play(hit) spin(0.05) shuffle(deck) spin(0.05) y+=vy*dt perlin(u,v) SPAWN scale=1+p*0.2 MINIBOSS WALK x+=vx*dt seed^=seed<<13 placeChest() dt=1/60 GAMELOOP fadeOut(1.0) STAMINA CRAFTING GRAVITY draw(sprite) PARRY GARBAGE play(hit) PLAYTEST if(see(player)) wave=sin(time*2) update(dt) SPRITE normalize(v) state=CHASE vy+=G*dt shake(0.3) perlin(u,v) fadeOut(1.0) RAYCAST LIGHTMAP MINIBOSS cross(a,b) sign(vx) FRAME if(combo>best) WEBGL moveTo(next) ease=t*t SKELETON vy=min(vy,MAX) ox=cx+cos(a)*d if(see(player)) perlin(u,v) orbit(t) shuffle(deck) FADE oy=cy+sin(a)*d present() dot(n,l) HITBOX MESH vx*=0.9 UPGRADE hitstop(0.05) play(hit) spin(0.05) shuffle(deck) spin(0.05) y+=vy*dt perlin(u,v) SPAWN scale=1+p*0.2 MINIBOSS WALK x+=vx*dt seed^=seed<<13 placeChest() dt=1/60 GAMELOOP fadeOut(1.0) STAMINA CRAFTING GRAVITY draw(sprite) PARRY GARBAGE play(hit) PLAYTEST if(see(player)) wave=sin(time*2) update(dt) SPRITE normalize(v) poll(input) y+=vy*dt PROFILER lives-- HEALTH HITBOX TWEEN pos=pos+vel SPAWN render() EASING draw(sprite) DROP aabb(a,b) TWEEN speed=8.0 LEVELUP ATTACK crit=rng()<0.1 TUTORIAL RESPAWN TWEEN VERTEX roll=rng(1,20) shake(0.3) SHADER if(lives==0) CHEST NOISE TUTORIAL score+=100 MEMORY PATROL BUGFIX orbit(t) if(combo>best) BUFF MANA fadeOut(1.0) HUD flood(x,y) perlin(u,v) update(dt) HEALTH flash(0.1) perlin(u,v) SEED state=IDLE sleep(rest) JUMP DODGE flee() neighbors(x,y) tile=map[idx] grid[y][x]=1 speed=8.0 RUN emit(particles) aggro+=1 MEMORY if(tile==WALL) noise(x,y) MESH die() TICK lvl=floor(exp/100) update(dt) WEBGL pick(table) sign(vx) state=IDLE score+=100 speed=8.0 clamp(v,0,1) IFRAMES if(roll>=15) poll(input) y+=vy*dt PROFILER lives-- HEALTH HITBOX TWEEN pos=pos+vel SPAWN render() EASING draw(sprite) DROP aabb(a,b) TWEEN speed=8.0 LEVELUP ATTACK crit=rng()<0.1 TUTORIAL RESPAWN TWEEN VERTEX roll=rng(1,20) shake(0.3) SHADER if(lives==0) CHEST NOISE TUTORIAL score+=100 MEMORY PATROL BUGFIX orbit(t) if(combo>best) BUFF MANA fadeOut(1.0) HUD flood(x,y) perlin(u,v) update(dt) HEALTH flash(0.1) perlin(u,v) SEED state=IDLE sleep(rest) JUMP DODGE flee() neighbors(x,y) tile=map[idx] grid[y][x]=1 speed=8.0 RUN emit(particles) aggro+=1 MEMORY if(tile==WALL) noise(x,y) MESH die() TICK lvl=floor(exp/100) update(dt) WEBGL pick(table) sign(vx) state=IDLE score+=100 speed=8.0 clamp(v,0,1) IFRAMES if(roll>=15) TICK play(hit) if(see(player)) TEXTURE sin(t)*r INVENTORY cooldown-=dt if(combo>best) WAVE VSYNC PROCGEN spawnRoom() FADE tween(x,to,0.4) DEADZONE FRAME PATROL SWARM lives-- combo*=2 lerp(a,b,t) fadeOut(1.0) if(lives==0) RAYCAST AGGRO vy=-JUMP orbit(t) BUFF LOOP MANA cos(t)*r play(hit) hp-=dmg WAVE dt=1/60 MINIBOSS draw(sprite) KNOCKBACK VIEWPORT scale=1+p*0.2 INVENTORY if(roll>=15) if(see(player)) fixedUpdate() VIEWPORT SPRITE cooldown-=dt FIXEDSTEP while(running) GRAVITY length(v) RESPAWN emit(particles) CAMERA speed=8.0 vy=min(vy,MAX) y+=vy*dt STAMINA crit=rng()<0.1 WEBGL HUD score+=100 connect(a,b) RIG ENEMY vy=-JUMP hitstop(0.05) MESH UPGRADE STAMINA lerp(a,b,t) SPRITE MESH PLAYTEST FPS ox=cx+cos(a)*d x+=vx*dt SEED alpha-=dt CRIT DEBUFF aggro+=1 exp+=50 tween(x,to,0.4) TICK play(hit) if(see(player)) TEXTURE sin(t)*r INVENTORY cooldown-=dt if(combo>best) WAVE VSYNC PROCGEN spawnRoom() FADE tween(x,to,0.4) DEADZONE FRAME PATROL SWARM lives-- combo*=2 lerp(a,b,t) fadeOut(1.0) if(lives==0) RAYCAST AGGRO vy=-JUMP orbit(t) BUFF LOOP MANA cos(t)*r play(hit) hp-=dmg WAVE dt=1/60 MINIBOSS draw(sprite) KNOCKBACK VIEWPORT scale=1+p*0.2 INVENTORY if(roll>=15) if(see(player)) fixedUpdate() VIEWPORT SPRITE cooldown-=dt FIXEDSTEP while(running) GRAVITY length(v) RESPAWN emit(particles) CAMERA speed=8.0 vy=min(vy,MAX) y+=vy*dt STAMINA crit=rng()<0.1 WEBGL HUD score+=100 connect(a,b) RIG ENEMY vy=-JUMP hitstop(0.05) MESH UPGRADE STAMINA lerp(a,b,t) SPRITE MESH PLAYTEST FPS ox=cx+cos(a)*d x+=vx*dt SEED alpha-=dt CRIT DEBUFF aggro+=1 exp+=50 tween(x,to,0.4) placeChest() if(lives==0) PROFILER placeChest() ENEMY BUGFIX SEED emit(particles) while(running) HURTBOX RESPEC seed^=seed>>17 rand()%6 spin(0.05) swapBuffers() FRICTION KEYFRAME LEVELUP MESH EASING GRAVITY if(near)attack() VSYNC coins++ RAYCAST atan2(dy,dx) noise(x,y) TILESET TILEMAP ATTACK HURTBOX PARALLAX SCREENSHAKE shuffle(deck) scale=1+p*0.2 VERTEX VSYNC abs(dx) PROCGEN tile=map[idx] if(see(player)) clamp(v,0,1) MINIMAP SHADER clamp(v,0,1) TWEEN combo*=2 shake(0.3) vy+=G*dt DEADZONE COOLDOWN PROFILER IFRAMES speed=8.0 vx*=0.9 CHECKPOINT DUNGEON BUFF PROCGEN DASH OVERLAY COLLIDER gameOver() orbit(t) MINIMAP orbit(t) ENEMY score+=100 DASH swapBuffers() hitstop(0.05) seed^=seed>>17 roll=rng(1,20) placeChest() if(lives==0) PROFILER placeChest() ENEMY BUGFIX SEED emit(particles) while(running) HURTBOX RESPEC seed^=seed>>17 rand()%6 spin(0.05) swapBuffers() FRICTION KEYFRAME LEVELUP MESH EASING GRAVITY if(near)attack() VSYNC coins++ RAYCAST atan2(dy,dx) noise(x,y) TILESET TILEMAP ATTACK HURTBOX PARALLAX SCREENSHAKE shuffle(deck) scale=1+p*0.2 VERTEX VSYNC abs(dx) PROCGEN tile=map[idx] if(see(player)) clamp(v,0,1) MINIMAP SHADER clamp(v,0,1) TWEEN combo*=2 shake(0.3) vy+=G*dt DEADZONE COOLDOWN PROFILER IFRAMES speed=8.0 vx*=0.9 CHECKPOINT DUNGEON BUFF PROCGEN DASH OVERLAY COLLIDER gameOver() orbit(t) MINIMAP orbit(t) ENEMY score+=100 DASH swapBuffers() hitstop(0.05) seed^=seed>>17 roll=rng(1,20) BUFF CRAFTING present() RESPAWN KEYFRAME TUTORIAL MINIMAP poll(input) ENEMY DELTATIME if(dist<r1+r2) tile=map[idx] PATROL NOISE PATROL present() PIXELART seed^=seed<<13 PLAYTEST scale=1+p*0.2 best=combo SHIELD RESPEC connect(a,b) CHECKPOINT RAYCAST oy=cy+sin(a)*d TRANSITION NOISE round(score) play(hit) if(roll>=15) y+=vy*dt JUMP PROFILER TILEMAP swapBuffers() hp-=dmg clamp(v,0,1) TUTORIAL PROFILER SPAWNER TRANSITION fixedUpdate() BUFF CRAFTING present() RESPAWN KEYFRAME TUTORIAL MINIMAP poll(input) ENEMY DELTATIME if(dist<r1+r2) tile=map[idx] PATROL NOISE PATROL present() PIXELART seed^=seed<<13 PLAYTEST scale=1+p*0.2 best=combo SHIELD RESPEC connect(a,b) CHECKPOINT RAYCAST oy=cy+sin(a)*d TRANSITION NOISE round(score) play(hit) if(roll>=15) y+=vy*dt JUMP PROFILER TILEMAP swapBuffers() hp-=dmg clamp(v,0,1) TUTORIAL PROFILER SPAWNER TRANSITION fixedUpdate() length(v) flee() TEXTURE JUMP PLAYTEST tween(x,to,0.4) LIGHTMAP SEED DEADZONE MESH aggro+=1 draw(sprite) aggro+=1 CHEST if(near)attack() ease=t*t overlap(r1,r2) for(i=0;i<n;i++) GRAVITY ox=cx+cos(a)*d alpha-=dt fadeOut(1.0) SPRITE PARRY ATTACK score+=100 flood(x,y) atan2(dy,dx) die() if(combo>best) next=path.shift() if(near)attack() COLLIDER TICK DELTATIME lives-- SEED SWARM PLAYTEST if(roll>=15) present() emit(particles) BUGFIX MINIMAP dt=1/60 SHIELD idx=y*W+x BUFF PARALLAX ATTACK FRICTION path=astar(a,b) FRICTION hp-=dmg lvl=floor(exp/100) roll=rng(1,20) dt=1/60 vy+=G*dt length(v) flee() TEXTURE JUMP PLAYTEST tween(x,to,0.4) LIGHTMAP SEED DEADZONE MESH aggro+=1 draw(sprite) aggro+=1 CHEST if(near)attack() ease=t*t overlap(r1,r2) for(i=0;i<n;i++) GRAVITY ox=cx+cos(a)*d alpha-=dt fadeOut(1.0) SPRITE PARRY ATTACK score+=100 flood(x,y) atan2(dy,dx) die() if(combo>best) next=path.shift() if(near)attack() COLLIDER TICK DELTATIME lives-- SEED SWARM PLAYTEST if(roll>=15) present() emit(particles) BUGFIX MINIMAP dt=1/60 SHIELD idx=y*W+x BUFF PARALLAX ATTACK FRICTION path=astar(a,b) FRICTION hp-=dmg lvl=floor(exp/100) roll=rng(1,20) dt=1/60 vy+=G*dt IFRAMES connect(a,b) COOLDOWN combo*=2 atan2(dy,dx) LOOP CHEST rand()%6 RESPAWN ATTACK NPC dot(n,l) UPGRADE FIXEDSTEP y+=vy*dt x+=vx*dt sign(vx) DELTATIME CRIT poll(input) round(score) sleep(rest) hp-=dmg clamp(v,0,1) fixedUpdate() SKELETON IFRAMES vx*=0.9 SAVEPOINT RESPAWN dist=hypot(dx,dy) sin(t)*r pos=pos+vel play(hit) rand()%6 DUNGEON overlap(r1,r2) wave=sin(time*2) LIGHTMAP poll(input) floor(x/TILE) play(hit) lerp(a,b,t) HURTBOX play(hit) shake(0.3) atan2(dy,dx) LIGHTMAP SPAWNER WAVE dot(n,l) while(running) vy=min(vy,MAX) placeChest() IFRAMES PATCH MESH pick(table) IFRAMES connect(a,b) COOLDOWN combo*=2 atan2(dy,dx) LOOP CHEST rand()%6 RESPAWN ATTACK NPC dot(n,l) UPGRADE FIXEDSTEP y+=vy*dt x+=vx*dt sign(vx) DELTATIME CRIT poll(input) round(score) sleep(rest) hp-=dmg clamp(v,0,1) fixedUpdate() SKELETON IFRAMES vx*=0.9 SAVEPOINT RESPAWN dist=hypot(dx,dy) sin(t)*r pos=pos+vel play(hit) rand()%6 DUNGEON overlap(r1,r2) wave=sin(time*2) LIGHTMAP poll(input) floor(x/TILE) play(hit) lerp(a,b,t) HURTBOX play(hit) shake(0.3) atan2(dy,dx) LIGHTMAP SPAWNER WAVE dot(n,l) while(running) vy=min(vy,MAX) placeChest() IFRAMES PATCH MESH pick(table) rand()%6 fadeOut(1.0) GAMELOOP sleep(rest) SKELETON GAMELOOP VELOCITY AGGRO if(near)attack() abs(dx) TILESET moveTo(next) SKILLTREE y+=vy*dt lives-- spin(0.05) DEBUFF CRIT SKELETON flash(0.1) ANIMATION pick(table) aabb(a,b) KEYFRAME neighbors(x,y) vx*=0.9 OVERLAY moveTo(next) TRANSITION KEYFRAME HITSTUN vy=-JUMP moveTo(next) while(running) perlin(u,v) if(see(player)) if(near)attack() if(near)attack() hp-=dmg moveTo(next) length(v) ENEMY sin(t)*r AGGRO crit=rng()<0.1 ox=cx+cos(a)*d KEYFRAME VIEWPORT lives-- sign(vx) HEALTH aabb(a,b) MINIBOSS GRAVITY ENEMY dt=1/60 die() DEBUFF dt=1/60 SEED cos(t)*r gameOver() SPAWN RUN MANA LIGHTMAP if(see(player)) STAMINA rand()%6 fadeOut(1.0) GAMELOOP sleep(rest) SKELETON GAMELOOP VELOCITY AGGRO if(near)attack() abs(dx) TILESET moveTo(next) SKILLTREE y+=vy*dt lives-- spin(0.05) DEBUFF CRIT SKELETON flash(0.1) ANIMATION pick(table) aabb(a,b) KEYFRAME neighbors(x,y) vx*=0.9 OVERLAY moveTo(next) TRANSITION KEYFRAME HITSTUN vy=-JUMP moveTo(next) while(running) perlin(u,v) if(see(player)) if(near)attack() if(near)attack() hp-=dmg moveTo(next) length(v) ENEMY sin(t)*r AGGRO crit=rng()<0.1 ox=cx+cos(a)*d KEYFRAME VIEWPORT lives-- sign(vx) HEALTH aabb(a,b) MINIBOSS GRAVITY ENEMY dt=1/60 die() DEBUFF dt=1/60 SEED cos(t)*r gameOver() SPAWN RUN MANA LIGHTMAP if(see(player)) STAMINA UPGRADE VIEWPORT MINIMAP VELOCITY die() sleep(rest) lvl=floor(exp/100) grid[y][x]=1 VERTEX JUMP wave=sin(time*2) WALK TILEMAP RAYCAST cross(a,b) tween(x,to,0.4) tween(x,to,0.4) aabb(a,b) ease=t*t SPAWNER dt=1/60 SPAWN SHADER CRAFTING sign(vx) vx*=0.9 clamp(v,0,1) orbit(t) neighbors(x,y) LOOT EASING LEVELUP abs(dx) dot(n,l) PROFILER next=path.shift() SKELETON vy=-JUMP TILEMAP if(near)attack() atan2(dy,dx) PARTICLE if(roll>=15) carve(room) speed=8.0 LIGHTMAP state=CHASE FADE present() FRAME TILEMAP vy=min(vy,MAX) orbit(t) UPGRADE VIEWPORT MINIMAP VELOCITY die() sleep(rest) lvl=floor(exp/100) grid[y][x]=1 VERTEX JUMP wave=sin(time*2) WALK TILEMAP RAYCAST cross(a,b) tween(x,to,0.4) tween(x,to,0.4) aabb(a,b) ease=t*t SPAWNER dt=1/60 SPAWN SHADER CRAFTING sign(vx) vx*=0.9 clamp(v,0,1) orbit(t) neighbors(x,y) LOOT EASING LEVELUP abs(dx) dot(n,l) PROFILER next=path.shift() SKELETON vy=-JUMP TILEMAP if(near)attack() atan2(dy,dx) PARTICLE if(roll>=15) carve(room) speed=8.0 LIGHTMAP state=CHASE FADE present() FRAME TILEMAP vy=min(vy,MAX) orbit(t) lerp(a,b,t) CULLING KNOCKBACK y+=vy*dt FRAME CHECKPOINT wave=sin(time*2) WEBGL next=path.shift() HITSTUN PROFILER RESPAWN flee() WALK TRANSITION grid[y][x]=1 SPAWN rand()%6 if(dist<r1+r2) noise(x,y) vx*=0.9 FIXEDSTEP RESPAWN draw(sprite) MANA abs(dx) VELOCITY JUMP seed^=seed<<13 PATROL shake(0.3) PROFILER alpha-=dt speed=8.0 LEVELUP angle+=PI/60 state=IDLE spawnRoom() lvl=floor(exp/100) dt=1/60 vy=-JUMP PATROL MEMORY BIOME CHEST state=CHASE if(tile==WALL) DEADZONE hp-=dmg placeChest() MESH WAVE round(score) shake(0.3) FRAME for(i=0;i<n;i++) IDLE BOSS abs(dx) lvl=floor(exp/100) SPRITE present() flood(x,y) RESPAWN present() sleep(rest) vy=min(vy,MAX) lerp(a,b,t) CULLING KNOCKBACK y+=vy*dt FRAME CHECKPOINT wave=sin(time*2) WEBGL next=path.shift() HITSTUN PROFILER RESPAWN flee() WALK TRANSITION grid[y][x]=1 SPAWN rand()%6 if(dist<r1+r2) noise(x,y) vx*=0.9 FIXEDSTEP RESPAWN draw(sprite) MANA abs(dx) VELOCITY JUMP seed^=seed<<13 PATROL shake(0.3) PROFILER alpha-=dt speed=8.0 LEVELUP angle+=PI/60 state=IDLE spawnRoom() lvl=floor(exp/100) dt=1/60 vy=-JUMP PATROL MEMORY BIOME CHEST state=CHASE if(tile==WALL) DEADZONE hp-=dmg placeChest() MESH WAVE round(score) shake(0.3) FRAME for(i=0;i<n;i++) IDLE BOSS abs(dx) lvl=floor(exp/100) SPRITE present() flood(x,y) RESPAWN present() sleep(rest) vy=min(vy,MAX) roll=rng(1,20) if(hp<=0) PATCH SKELETON shake(0.3) VERTEX seed^=seed>>17 grid[y][x]=1 MINIMAP vy=-JUMP fadeOut(1.0) lvl=floor(exp/100) FADE WEBGL MINIMAP carve(room) PARALLAX ENEMY HURTBOX cross(a,b) BUILD JUMP exp+=50 KNOCKBACK if(roll>=15) HUD clamp(v,0,1) RAYCAST TILESET dist=hypot(dx,dy) sleep(rest) aggro+=1 floor(x/TILE) SWARM while(running) if(lives==0) PROFILER if(near)attack() DROP clamp(v,0,1) gameOver() connect(a,b) if(roll>=15) PARRY HURTBOX update(dt) WALK state=IDLE overlap(r1,r2) BIOME LOOP VIEWPORT shuffle(deck) exp+=50 seed^=seed<<13 DEADZONE GRAVITY PATROL idx=y*W+x cooldown-=dt lives-- atan2(dy,dx) state=IDLE if(dist<r1+r2) onHit(dmg) spin(0.05) moveTo(next) flood(x,y) TWEEN SHIELD VSYNC render() BUGFIX gameOver() SCREENSHAKE roll=rng(1,20) if(hp<=0) PATCH SKELETON shake(0.3) VERTEX seed^=seed>>17 grid[y][x]=1 MINIMAP vy=-JUMP fadeOut(1.0) lvl=floor(exp/100) FADE WEBGL MINIMAP carve(room) PARALLAX ENEMY HURTBOX cross(a,b) BUILD JUMP exp+=50 KNOCKBACK if(roll>=15) HUD clamp(v,0,1) RAYCAST TILESET dist=hypot(dx,dy) sleep(rest) aggro+=1 floor(x/TILE) SWARM while(running) if(lives==0) PROFILER if(near)attack() DROP clamp(v,0,1) gameOver() connect(a,b) if(roll>=15) PARRY HURTBOX update(dt) WALK state=IDLE overlap(r1,r2) BIOME LOOP VIEWPORT shuffle(deck) exp+=50 seed^=seed<<13 DEADZONE GRAVITY PATROL idx=y*W+x cooldown-=dt lives-- atan2(dy,dx) state=IDLE if(dist<r1+r2) onHit(dmg) spin(0.05) moveTo(next) flood(x,y) TWEEN SHIELD VSYNC render() BUGFIX gameOver() SCREENSHAKE
投稿された zip は、ClamAV による既知マルウェアの検出に加えて、Zip Slip・圧縮爆弾・シンボリックリンク・拡張子偽装・外部スクリプト読み込みを検査します。危険と判定したファイルは公開領域に配置されず、判定理由が投稿者に 開示されます。検査エンジンが停止している場合、投稿は「問題なし」ではなく受付停止として扱われます。
検査の8段階
危険なものほど早い段階で、かつ展開する前に弾く順序になっています。
投稿ゲームのオリジン分離
ウイルス検査は既知の脅威しか止められません。シグネチャにない 新種が 1 本すり抜けた時点で終わり、では守りとして成立しないため、 すり抜けたものの被害を封じ込める仕組みを別に用意しています。
投稿された HTML5 ゲームは任意の JavaScript を実行します。これをサイト本体と 同じドメインで配信すると、ゲーム内のスクリプトから本体の Cookie・ localStorage・DOM がそのまま読めてしまい、ログイン中の利用者になりすませます。 そこで GAMEYARD では、投稿ゲームをサイト本体とは別のドメインから配信し、sandbox 属性付きのフレーム内で動かしています。
遮断するのはブラウザの同一生成元ポリシーであって、当サイトのコードではありません。こちらの実装にミスがあっても守りが消えないのが、この構成を選んだ 理由です。
| 配信元 | サイト本体とは別ドメイン(サブドメインではなく別ドメイン) |
|---|
| 埋め込み | sandbox="allow-scripts allow-same-origin allow-pointer-lock" 別オリジンのため allow-same-origin で復元されるのは配信ドメイン 自身のオリジンで、本体へは到達できません。ゲームのセーブデータ保存を 成立させるために必要な指定です。 |
|---|
| 外部通信 | CSP の connect-src 'self' で禁止。プレイヤーの情報を 外部へ送信する経路を塞いでいます。 |
|---|
| 埋め込み元の制限 | CSP の frame-ancestors で当サイト以外からの埋め込みを拒否。 |
|---|
| そのほか | X-Content-Type-Options: nosniff / Referrer-Policy: no-referrer / Permissions-Policy で位置情報・カメラ・マイク・決済を無効化 |
|---|
なお、公開に使うのはブラウザから再送されたファイルではなく、サーバーが検査時に 保管した実体です。検査を通した後に別のファイルへすり替える余地をなくすためです。
設計上の判断
クライアント側の検証は安全性の根拠にしない
投稿フォームにも入力チェックはありますが、これは入力の作法を整えるためのもので、 安全性の判断は一切していません。ブラウザ側の検証は投稿者が自由に迂回できるため、 サーバーが自分で検査した結果だけを根拠にします。
圧縮爆弾は検査エンジンにも渡さない
構造検査で圧縮爆弾と判定したファイルは、ClamAV にも渡しません。渡せば今度は 検査エンジンが展開を始めて CPU とメモリを消費し、攻撃者の狙いどおりになります。 すでに拒否が確定しているものに計算資源を使わないのが正しい対応です。
検査できないものは通さない
パスワード保護された zip、検査エンジンの停止、エンジンによる検査の打ち切り。 いずれも「危険とは断定できない」状態ですが、「安全と確認できていない」状態でも あります。この場合は公開しません。判断に迷ったら安全側に倒します。
受付上限は「検査が実際に届く範囲」から決めている
zip 全体 200MB・ファイル数 5,000・単一ファイル 200MB という上限は、切りのいい 数字ではなく実測から決めた値です。検査エンジンには自身の上限があり、それを 超えたファイルは中身を一切開かずに打ち切られます(実測: 360MB の zip は 1.4 秒で打ち切り)。またファイル数 21,000 件では検査が時間上限に達します。 受け付けてから「検査できませんでした」と返すのは、投稿者に数百 MB を無駄に 転送させるうえ、こちらも「検査済み」と言えない作品を抱えることになります。
上限の内側で検査が本当に奥まで届いているかは、既知のテスト検体(EICAR)を アーカイブの末尾・入れ子の内側・5,000 ファイル目の後ろに置いて検出されることを 自動試験で確認しています。上限を変えるときはこの試験を通すことが条件です。
「検出できる状態か」を定期的に確かめる
検査エンジンに接続できても、定義データベースが壊れていれば「何も検出しない エンジン」として動き続けます。その状態の応答は「すべて問題なし」と区別が つきません。そのため起動時と 5 分ごとに、検出されるはずのテスト検体を実際に 投げて、検出されることを確認しています。確認できない間は投稿を受け付けません。
拒否の理由は投稿者に開示する
「危険でした」とだけ返しても投稿者は修正できません。どの段階の、何という検査 ID が、どのファイルに反応したかまで開示します。誤検知だった場合に異議を申し立てる 材料にもなります。
よくある質問
投稿したファイルはどのように検査されますか?
受付・ハッシュ照合・構造検査・ClamAV によるシグネチャ検査・制限付き展開・ファイル実体判定・静的コンテンツ解析・判定の8段階で検査します。検査結果は検査 ID つきで投稿者に開示され、どの段階の何という検査がどのファイルに反応したかを確認できます。
検査にはどのくらい時間がかかりますか?
常駐しているウイルス検査デーモンに直接データを渡す方式のため、一般的な HTML5 ゲーム(数MB程度)で数十ミリ秒から数百ミリ秒です。検査のたびにウイルス定義を読み込む方式だと1回あたり20秒以上かかるため、常駐方式を採用しています。
誤検知された場合はどうすればよいですか?
検査結果には検査 ID(例: external-script、ext-mismatch)と対象ファイル名が表示されます。その情報を添えてお問い合わせください。難読化の検出などはミニファイ済みのゲームエンジンで正常に反応することがあるため、その場合は「要目視確認」として運営が個別に判断します。
ウイルス検査エンジンが停止しているときはどうなりますか?
投稿を受け付けません。検査エンジンに接続できない状態を「問題なし」として扱うと、検査を回避したい攻撃者にとって最も都合のよい抜け道になります。エンジンが応答しない場合は明示的に公開不可として扱います。
検査で拒否されたファイルはどうなりますか?
隔離領域に保管し、原因調査と再投稿時のハッシュ照合に使います。公開領域には一切配置されません。