Skip to content

Presigned POST URL ${filename} substitution potential bug #7191

@GarrettBeatty

Description

@GarrettBeatty

Checkboxes for prior research

Describe the bug

I am working on implementing presigned post urls for .net sdk and i noticed something while testing the javascript sdk.

When using ${filename} variable in the success_action_redirect field with createPresignedPost(), S3 rejects the upload with AccessDenied error. The issue is that the JavaScript SDK adds an exact match condition for the success_action_redirect field (without replacing ${filename} with the actual filename, but S3 tries to substitute ${filename} with the actual filename during upload, causing a policy condition mismatch.

If you check the repro steps below, everything works when the success_action_redirect is some regular text. but as soon as i include ${filename} at the end of it, it fails, which suggests s3 is replacing that on the backend with the actual file name. But the issue javascript sdk isnt updating the condition before sending it

Regression Issue

  • Select this option if this issue appears to be a regression.

SDK version number

"@aws-sdk/client-s3": "^3.445.0", "@aws-sdk/s3-presigned-post": "^3.445.0", "node-fetch": "^3.3.2", "form-data": "^4.0.0"

Which JavaScript Runtime is this issue in?

Node.js

Details of the browser/Node.js/ReactNative version

v22.14.0

Reproduction Steps

import { 
  S3Client, 
  CreateBucketCommand, 
  ListObjectsV2Command, 
  DeleteObjectsCommand,
  DeleteBucketCommand,
  HeadBucketCommand,
  waitUntilBucketExists
} from '@aws-sdk/client-s3';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
import fetch from 'node-fetch';
import FormData from 'form-data';

// Configuration
const REGION = 'us-west-2';
const BUCKET_NAME = `s3-filename-variable-test-${Date.now()}`;
const s3Client = new S3Client({ region: REGION });

async function setupBucket() {
  try {
    console.log(`Setting up test bucket: ${BUCKET_NAME}`);
    
    // Check if bucket already exists
    try {
      await s3Client.send(new HeadBucketCommand({ Bucket: BUCKET_NAME }));
      console.log(`Bucket ${BUCKET_NAME} already exists, using it for tests`);
    } catch (err) {
      // Create bucket if it doesn't exist
      console.log(`Creating bucket ${BUCKET_NAME}...`);
      await s3Client.send(new CreateBucketCommand({ 
        Bucket: BUCKET_NAME,
        CreateBucketConfiguration: {
          LocationConstraint: REGION !== 'us-east-1' ? REGION : undefined
        }
      }));
      
      // Wait for bucket to be available
      await waitUntilBucketExists(
        { client: s3Client, maxWaitTime: 60 },
        { Bucket: BUCKET_NAME }
      );
      console.log(`Bucket ${BUCKET_NAME} created successfully`);
    }
    
    return true;
  } catch (err) {
    console.error('Error setting up bucket:', err);
    return false;
  }
}

async function cleanupBucket() {
  try {
    console.log(`\nCleaning up bucket: ${BUCKET_NAME}`);
    
    // List all objects in the bucket
    const listResult = await s3Client.send(new ListObjectsV2Command({
      Bucket: BUCKET_NAME
    }));
    
    if (listResult.Contents && listResult.Contents.length > 0) {
      console.log(`Found ${listResult.Contents.length} objects to delete`);
      
      // Delete all objects
      await s3Client.send(new DeleteObjectsCommand({
        Bucket: BUCKET_NAME,
        Delete: {
          Objects: listResult.Contents.map(obj => ({ Key: obj.Key }))
        }
      }));
      console.log('All objects deleted');
    } else {
      console.log('No objects to delete');
    }
    
    // Delete the bucket
    await s3Client.send(new DeleteBucketCommand({
      Bucket: BUCKET_NAME
    }));
    console.log(`Bucket ${BUCKET_NAME} deleted successfully`);
    
    return true;
  } catch (err) {
    console.error('Error during cleanup:', err);
    return false;
  }
}

async function testRedirect(redirectUrl) {
  try {
    console.log(`\nTesting with redirect URL: "${redirectUrl}"`);
    
    // Create presigned POST with success_action_redirect
    const { url, fields } = await createPresignedPost(s3Client, {
      Bucket: BUCKET_NAME,
      Key: `test-${Date.now()}.txt`,
      Fields: {
        'Content-Type': 'text/plain',
        'success_action_redirect': redirectUrl
      },
      Conditions: [
        ['content-length-range', 1, 1048576]
      ],
      Expires: 600
    });
    
    // Decode policy to see conditions
    const policyBase64 = fields.Policy;
    const policyJson = Buffer.from(policyBase64, 'base64').toString('utf-8');
    const policy = JSON.parse(policyJson);
    
    console.log('\nGenerated policy conditions:');
    policy.conditions.forEach(condition => {
      console.log(JSON.stringify(condition));
    });
    
    // Find the success_action_redirect condition
    const redirectCondition = policy.conditions.find(
      c => typeof c === 'object' && c['success_action_redirect'] !== undefined
    );
    
    console.log('\nSuccess_action_redirect condition:', 
      redirectCondition ? JSON.stringify(redirectCondition) : 'None found');
    
    // Try uploading
    console.log('\nAttempting upload...');
    const form = new FormData();
    
    // Add all fields from the presigned POST response
    Object.entries(fields).forEach(([key, value]) => {
      form.append(key, value);
    });
    
    // Add file content
    const testContent = 'Test content for filename variable bug reproduction';
    form.append('file', Buffer.from(testContent), {
      filename: 'test.txt',
      contentType: 'text/plain'
    });
    
    // Perform the upload
    const response = await fetch(url, {
      method: 'POST',
      body: form
    });
    
    console.log(`Upload status: ${response.status}`);
    if (!response.ok) {
      const text = await response.text();
      console.log(`Error response: ${text}`);
      return false;
    } else {
      console.log('Upload successful!');
      return true;
    }
  } catch (err) {
    console.error('Error during test:', err);
    return false;
  }
}

async function main() {
  try {
    console.log('S3 ${filename} Variable Bug Reproduction');
    console.log('======================================\n');
    
    // Setup the test bucket
    const setupSuccess = await setupBucket();
    if (!setupSuccess) {
      console.error('Failed to set up test bucket, aborting tests');
      return;
    }
    
    // Test 1: With ${filename} in success_action_redirect (will fail)
    console.log('\n== TEST 1: With ${filename} in success_action_redirect ==');
    const test1Result = await testRedirect('https://example.com/success?file=${filename}');
    console.log(`\nTEST 1 RESULT: ${test1Result ? 'PASSED' : 'FAILED'}`);
    if (test1Result) {
      console.log('Note: This test was expected to fail but passed. The issue may have been fixed.');
    } else {
      console.log('Note: This test failed as expected, confirming the issue exists.');
    }
    
    // Test 2: Without ${filename} (control test - should work)
    console.log('\n== TEST 2: Without ${filename} (control test) ==');
    const test2Result = await testRedirect('https://example.com/success');
    console.log(`\nTEST 2 RESULT: ${test2Result ? 'PASSED' : 'FAILED'}`);
    if (!test2Result) {
      console.log('Warning: Control test failed. There may be other issues with your setup.');
    }
    
    // Clean up resources
    await cleanupBucket();
    
    console.log('\nTest completed. If Test 1 failed and Test 2 passed, the issue is reproduced.');
  } catch (err) {
    console.error('Unexpected error:', err);
  }
}

// Run the test
main().catch(console.error);

Observed Behavior

the ${filename} is not replaced by the key value when ${filename} is included in one of the fields.

Expected Behavior

The AWS JavaScript SDK should detect ${filename} in any field (including success_action_redirect) and avoid generating exact match conditions that would interfere with S3's variable substitution. Theres currently logic that does this with the key https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/src/createPresignedPost.ts#L82 but im pretty sure it should be applied everywhere

Possible Solution

update https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/src/createPresignedPost.ts#L82 to replace for all fields instead of just the key

Additional Information/Context

No response

Metadata

Metadata

Assignees

Labels

bugThis issue is a bug.cross-sdkp2This is a standard priority issuequeuedThis issues is on the AWS team's backlog

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions