Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/img.cgi
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/img.cgi

added on local at 2026-07-01 13:47:21

Added
+120
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to d78b66f195ef
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# WebSTLs -- image serving CGI.
4#
5# /img.cgi?id=<16-char token> -> page_images entry (page editor)
6# /img.cgi?m=<model_image_id> -> model_images entry (upload form)
7#
8# Looks up the requested record, resolves the on-disk file path, and
9# streams the bytes with the right Content-Type. The on-disk location
10# is never revealed in any response. Direct HTTP access to /uploads/
11# is also blocked by .htaccess as defense-in-depth.
12#
13# No authentication is required for either mode: storefront pages are
14# public, so the images embedded in them are too. Token mode has 64
15# bits of entropy; m-mode uses auto-increment ids and is enumerable
16# but that matches the public-by-design model of the storefront.
17#======================================================================
18use strict;
19use warnings;
20
21use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com';
22use CGI;
23use MODS::DBConnect;
24use MODS::AffSoft::Config;
25
26my $q = CGI->new;
27my $db = MODS::DBConnect->new;
28my $config = MODS::AffSoft::Config->new;
29my $DB = $config->settings('database_name');
30
31sub not_found {
32 print "Status: 404 Not Found\nContent-Type: text/plain\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
33 print "image not found\n";
34 exit;
35}
36
37my $path;
38my $mime;
39
40if (my $mid_param = $q->param('m')) {
41 # ---- Model image mode ---------------------------------------
42 my $mid = $mid_param;
43 $mid =~ s/[^0-9]//g;
44 not_found() unless $mid;
45
46 my $dbh = $db->db_connect();
47 my $row = $db->db_readwrite($dbh,
48 qq~SELECT storage_key FROM `${DB}`.model_images WHERE id='$mid' LIMIT 1~,
49 $ENV{SCRIPT_NAME}, __LINE__);
50 $db->db_disconnect($dbh);
51
52 not_found() unless $row && $row->{storage_key};
53 my $key = $row->{storage_key};
54 # Reject any path traversal -- storage_key must be a relative path
55 # inside uploads/, no .. components, no leading slash.
56 not_found() if $key =~ /\.\./;
57 not_found() if $key =~ m{^/};
58 not_found() unless $key =~ m{^uploads/};
59 $path = "/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/$key";
60
61 # Derive mime from extension.
62 my ($ext) = lc($key) =~ /\.([a-z0-9]+)$/;
63 my %ext_mime = (
64 jpg => 'image/jpeg', jpeg => 'image/jpeg',
65 png => 'image/png', gif => 'image/gif',
66 webp => 'image/webp',
67 );
68 $mime = $ext_mime{$ext || ''} || 'application/octet-stream';
69} else {
70 # ---- Page image (token) mode --------------------------------
71 my $id = $q->param('id') || '';
72 $id =~ s/[^a-zA-Z0-9]//g;
73 not_found() unless length($id) >= 8 && length($id) <= 32;
74
75 my $dbh = $db->db_connect();
76 my $row = $db->db_readwrite($dbh,
77 qq~SELECT storefront_id, filename, mime_type, size_bytes
78 FROM `${DB}`.page_images
79 WHERE id='$id'
80 LIMIT 1~,
81 $ENV{SCRIPT_NAME}, __LINE__);
82 $db->db_disconnect($dbh);
83
84 not_found() unless $row && $row->{filename};
85
86 my $filename = $row->{filename};
87 not_found() if $filename =~ m{[/\\]} || $filename =~ /\.\./;
88
89 my $sid = $row->{storefront_id};
90 $sid =~ s/[^0-9]//g;
91 not_found() unless $sid;
92
93 $path = "/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/uploads/$sid/$filename";
94 $mime = $row->{mime_type} || 'application/octet-stream';
95}
96
97not_found() unless -r $path;
98
99my $size = -s $path;
100# Whitelist mime types -- never echo back an arbitrary string.
101my %ok_mime = map { $_ => 1 } qw(image/jpeg image/png image/gif image/webp);
102$mime = 'application/octet-stream' unless $ok_mime{$mime};
103
104# Caching: the id is part of the URL and is unique per upload, so the
105# bytes for any given id are immutable. Long cache + immutable hint.
106print "Content-Type: $mime\n";
107print "Content-Length: $size\n";
108print "Cache-Control: public, max-age=31536000, immutable\n";
109print "X-Content-Type-Options: nosniff\n";
110print "\n";
111
112binmode STDOUT;
113open(my $fh, '<', $path) or not_found();
114binmode $fh;
115my $buf;
116while (read($fh, $buf, 65536)) {
117 print STDOUT $buf;
118}
119close $fh;
120exit;
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help